# Featureflip > Feature flag platform for modern development teams. Roll out features gradually, run A/B experiments, and ship with confidence. Free to start, with SDKs for JavaScript, Python, C#, Java, Go, PHP, Ruby, Swift, Flutter, Android, and React. This file contains the full text of Featureflip's site, intended for AI consumption. It is generated from the built pages, so it cannot drift from what the site says. --- ## Source: https://featureflip.io/ # Featureflip — Feature flags without the tech debt # Feature flags without the tech debt When a rollout finishes, a GitHub Action opens one reviewable pull request per dead flag and removes the code path in thirteen languages, with no LLM involved. Percentage rollouts, targeting, A/B tests and kill switches come with it, across 13 SDKs. One flat price with nothing metered. No active users, no service connections, no evaluation caps, no overage. [Get Started Free](https://app.featureflip.io/signup) Free forever for up to 3 people. No credit card required. $ `npm install @featureflip/browser` Copy Running something else? There are [12 more SDKs](/docs/sdks/overview/) . app.js ```js import { FeatureflipClient } from '@featureflip/browser'; const client = FeatureflipClient.get({ clientKey: 'sdk_client_a1b2c3d4', }); await client.initialize(); if (client.boolVariation('new-checkout', false)) { showNewCheckout(); } ``` ## The whole life of a flag Create it from your editor, ramp it from the dashboard, and let the cleanup Action open the pull request that takes it back out. You review it. The merge archives the flag. - 1 Create agent → featureflip create_flag("new-checkout") Created. Disabled in every environment. Ask your agent for it over MCP, or declare it in Terraform. New flags land disabled in every environment, so nothing moves until you say so. Give it an expiry date on the way in. [Create flags over MCP →](/docs/integrations/mcp/) - 2 Ramp new-checkout 10% 50% 100% production · percentage rollout Take the rollout from 10% to 100% in the dashboard, through the API, or by asking the agent. Each change streams to your SDKs over SSE. Nothing redeploys. [Rollout strategies →](/docs/concepts/rollout-strategies/) - 3 Detect production Active Stale Dead Fully rolled out. One variation to 99.9% of evaluations. Featureflip reads the evaluation traffic each flag actually receives, hourly, per environment. A signal that holds for 30 days marks the flag stale. At 90 days it is dead. A passed expiry date marks it stale right away. [Staleness signals →](/docs/concepts/stale-flags/) - 4 Remove Remove dead feature flag: new-checkout featureflip/remove-flag/new-checkout Merged. Flag archived. The Action opens one reviewable pull request per dead flag, rewriting the code with AST transforms and no LLM, so the same repository always produces the same diff. Merge it and the flag is archived. Whatever it declines, like a flag read behind your own wrapper, your agent removes over MCP. [Flag cleanup Action →](/docs/integrations/flag-cleanup-action/) [Remove over MCP →](/docs/integrations/mcp/) When a flag expires or goes dead, its owner gets one cleanup email, or your admins do if nobody owns it. Expiry dates come with every plan, cleanup emails start on Team, and named flag owners start on Pro. [Expiry dates, owners and cleanup emails →](/product/features/flag-lifecycle/) ## Ship the flag. Then get it out of your codebase. Toggle, target, and roll out features from a real-time dashboard built for engineers who ship often. Staleness signals - Fully rolled out Enabled and serving one variation to 99.9%+ of evaluations. - Rolled back Disabled, with at most one variation served. - No traffic Enabled but receiving zero evaluations. - Never used Created and never evaluated in this environment. - Past expiry date The expiry date set on the flag has passed. Stale right away, no waiting period. Stale after 30 days Dead after 90 days ### Dead flags leave in a pull request A GitHub Action opens one reviewable pull request per dead flag, deletes the code path across thirteen languages with no LLM involved, and archives the flag when you merge. The detection behind it needs nothing installed and runs hourly, per environment, from the evaluation traffic each flag actually receives. [How removal works →](/blog/automated-feature-flag-removal/) [Stale flag detection →](/docs/concepts/stale-flags/) [MCP server →](/docs/integrations/mcp/) ```hcl resource "featureflip_feature_flag" "new_checkout" { project = "web" key = "new-checkout" name = "New Checkout Flow" type = "Boolean" } resource "featureflip_flag_environment" "prod" { project = "web" flag = featureflip_feature_flag.new_checkout.key environment = "production" enabled = true } ``` ### Manage flags as code Declare projects, flags, targeting rules, segments, and webhooks in Terraform, then apply them like the rest of your infrastructure. Flag changes get reviewed in a pull request, promote cleanly between environments, and stay reproducible. [Terraform provider →](/docs/integrations/terraform/) ### Instant rollouts Toggle features on or off across every environment in real time. No deployments, no restarts, no waiting — your change is live everywhere within milliseconds. [Kill switches →](/use-cases/kill-switch/) ### Gradual, percentage-based rollouts Ship to 1%, then 10%, then 100%. Watch metrics with every step. If anything looks off, roll back by moving the slider — no re-deploy required. [Progressive rollouts →](/use-cases/progressive-rollouts/) [Migrating to a new LLM →](/use-cases/feature-flags-for-ai/) ### Surgical targeting Combine attributes, segments, and plan data into AND/OR condition groups. Target by region, plan, cohort, or any custom attribute your app tracks. [Feature entitlements →](/use-cases/entitlements/) [Beta cohorts →](/use-cases/beta-programs/) ### Multi-environment by default Keep development and production configurations fully isolated. Promote the exact state forward when you are ready — or keep them intentionally different. ## Thirteen SDKs, and flag cleanup works with every one. Drop-in libraries in 13 languages and frameworks. Server SDKs evaluate in process, so a flag check costs a function call and no network round trip, returning in well under a millisecond. Client SDKs stream live updates to the browser. - [JavaScript](/docs/sdks/javascript/) - [Node.js](/docs/sdks/nodejs/) - [Python](/docs/sdks/python/) - [Go](/docs/sdks/go/) - [Java](/docs/sdks/java/) - [C#](/docs/sdks/csharp/) - [Ruby](/docs/sdks/ruby/) - [PHP](/docs/sdks/php/) - [Swift](/docs/sdks/swift/) - [Flutter](/docs/sdks/flutter/) - [Android](/docs/sdks/android/) - [React](/docs/sdks/react/) - [Browser](/docs/sdks/browser/) Because a server SDK reads from an in-memory copy of your configuration, it keeps serving flags when Featureflip is unreachable and re-syncs on its own once we are back. We wrote up [what separates an SDK that recovers from one that needs a restart](/blog/feature-flag-outage-recovery/) . Prefer a vendor-neutral API? Featureflip also ships [OpenFeature](/docs/integrations/openfeature/) providers for Node.js, .NET, Python, Go, Java and PHP. ## How much does Featureflip cost? Start free, scale as you grow. Billing interval Monthly Annual Save ~20% Every plan includes percentage rollouts, targeting rules, kill switches, all 13 SDKs, the Management API, the Terraform provider, and flag expiry dates. Paid plans add scheduled changes and cleanup emails, Pro adds flag owners, and Business adds single sign-on through your own identity provider. Beyond that, plans differ only in scale. ### Free $0 /mo Free forever, and built to run in production - 10 flags per project - 2 environments per project - 1 project - Up to 3 team members - Flag expiry dates - 7-day audit logs - 1 webhook endpoint - Management API access - Community support [Get Started Free](https://app.featureflip.io/signup) ### Team $49 $39 /mo billed annually For small teams shipping features together - Unlimited flags - 3 environments per project - 3 projects - Up to 5 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - 30-day audit logs - 3 webhook endpoints - Management API access - Email support [Get Started](https://app.featureflip.io/signup) ### Pro $99 $79 /mo billed annually For teams running several products - Unlimited flags - 5 environments per project - 10 projects - Up to 10 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - Flag owners - 30-day audit logs - 3 webhook endpoints - Management API access - Email support [Get Started](https://app.featureflip.io/signup) ### Business $249 $199 /mo billed annually For growing organizations that need more room - Unlimited flags - Unlimited environments - 25 projects - Up to 25 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - Flag owners - SSO with your identity provider (OIDC) - 90-day audit logs - Unlimited webhook endpoints - Management API access - Priority support [Get Started](https://app.featureflip.io/signup) Need more? [Contact us](mailto:sales@featureflip.io) for Enterprise pricing. Already using LaunchDarkly? Featureflip is flat per plan, with nothing metered: no charge for active users, service connections, evaluation volume, or overage. Put your own numbers through the [LaunchDarkly cost calculator](/tools/launchdarkly-cost-calculator/) to see what that changes, or read the [full comparison](/compare/featureflip-vs-launchdarkly/) on pricing math, feature parity, and migration. Moving off Flagsmith or ConfigCat instead? The guides to [Flagsmith alternatives](/alternatives/flagsmith-alternative/) and [ConfigCat alternatives](/alternatives/configcat-alternative/) walk through choosing a replacement for each. ## Frequently asked questions What are feature flags? Feature flags (also called feature toggles) let you turn functionality on or off in a running application without deploying new code. That decouples deploy from release: you can merge to production with a feature switched off, then turn it on for 1% of users, expand to everyone, serve a different variation to one cohort for an A/B test, or kill it the moment a metric moves the wrong way. No rebuild, and the change reaches your SDKs in seconds. Is Featureflip free to use? Yes! The Free plan is free forever and includes 1 project, 10 flags, and 2 environments. Paid plans start at $49/month ($39/month billed annually) for teams that need more projects, members, and advanced features. What programming languages are supported? Featureflip provides official server-side SDKs for JavaScript/TypeScript, Node.js, Python, C#, Java, Go, PHP, and Ruby, plus client-side SDKs for React, Browser, Swift, Flutter, and Android. You can also integrate directly with the Evaluation API using any HTTP client. How is Featureflip different from other feature flag platforms? Featureflip closes the loop on a flag. Once a flag is finished, a GitHub Action opens one reviewable pull request per dead flag, removes the code path in thirteen languages with no LLM involved, and archives the flag when you merge. The price is flat per plan and nothing is metered, so there is no charge for active users, service connections, evaluation volume, or overage. Percentage rollouts, targeting, kill switches, and all 13 SDKs are included at every tier, including the Free plan, which is meant to run in production rather than expire after a trial. Can I manage feature flags through an API? Yes. The Management API exposes everything the dashboard does over REST: create and toggle flags, manage environments, targeting rules, segments, and SDK keys. It is included on every plan, changes stream to your SDKs live, and every API change is recorded in the audit log. Webhook subscriptions live in the same API, so your deploy pipeline or chat channel can get a signed request the moment a flag changes. What happens to my app if Featureflip goes down? It keeps serving flags. Server SDKs hold your flag configuration in memory and evaluate it inside your own process, so a flag check never waits on a call to us. If Featureflip becomes unreachable, the SDK carries on evaluating against the last configuration it received, and any flag it cannot resolve returns the default you passed at the call site. It also reconnects on its own, retrying with capped exponential backoff and dropping to polling when a stream will not hold. Every reconnect pulls a full configuration snapshot, so a flag you changed during the gap gets picked up without anyone restarting a process. ## Ship your first flag in about five minutes Create a flag, drop in the SDK for your stack, and toggle it live. One flat price when you outgrow the free plan, whatever your team size or traffic. [Get Started Free](https://app.featureflip.io/signup) [Read the docs](/docs/) Free forever for up to 3 people. No credit card required. --- ## Source: https://featureflip.io/pricing/ # Pricing — Feature Flag Plans from Free to Business # Simple, transparent pricing Start free, scale as you grow. No per-seat fees, no hidden costs. Featureflip uses **flat pricing**: a fixed monthly price per plan, with no per-seat charges and no per-evaluation fees. The number on the plan is the number on your bill, however much traffic you serve. There are four plans, sized by how many people ship flags and how many products they ship them to. **Free** never expires and covers up to three people on one project, with no credit card and no trial window. **Team** is the next step when a fourth person joins, when two environments stop being enough, or when a one-week audit log no longer covers your compliance review. It also adds scheduled changes. **Pro** gives up to ten people room for ten projects, and adds flag owners so each cleanup email reaches whoever owns the flag. For the full cost breakdown across six vendors, read our guide to [feature flag flat pricing versus per-seat and usage-based billing](/blog/feature-flag-pricing-explained/) . **Business** adds the headroom most growing engineering organizations need: more projects, unlimited environments, longer audit retention, [single sign-on through your own identity provider](/docs/guides/single-sign-on/) , and priority response. Annual billing on any paid plan trims roughly 20% and matches how most teams budget software. If none of the plans fit — for example, you need on-premise deployment, custom data retention, or a contracted SLA — [Enterprise](mailto:sales@featureflip.io) pricing is negotiated directly. Billing interval Monthly Annual Save ~20% Every plan includes percentage rollouts, targeting rules, kill switches, all 13 SDKs, the Management API, the Terraform provider, and flag expiry dates. Paid plans add scheduled changes and cleanup emails, Pro adds flag owners, and Business adds single sign-on through your own identity provider. Beyond that, plans differ only in scale. ### Free $0 /mo Free forever, and built to run in production - 10 flags per project - 2 environments per project - 1 project - Up to 3 team members - Flag expiry dates - 7-day audit logs - 1 webhook endpoint - Management API access - Community support [Get Started Free](https://app.featureflip.io/signup) ### Team $49 $39 /mo billed annually For small teams shipping features together - Unlimited flags - 3 environments per project - 3 projects - Up to 5 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - 30-day audit logs - 3 webhook endpoints - Management API access - Email support [Get Started](https://app.featureflip.io/signup) ### Pro $99 $79 /mo billed annually For teams running several products - Unlimited flags - 5 environments per project - 10 projects - Up to 10 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - Flag owners - 30-day audit logs - 3 webhook endpoints - Management API access - Email support [Get Started](https://app.featureflip.io/signup) ### Business $249 $199 /mo billed annually For growing organizations that need more room - Unlimited flags - Unlimited environments - 25 projects - Up to 25 team members - Scheduled changes - Flag expiry dates - Flag cleanup emails - Flag owners - SSO with your identity provider (OIDC) - 90-day audit logs - Unlimited webhook endpoints - Management API access - Priority support [Get Started](https://app.featureflip.io/signup) Need more? [Contact us](mailto:sales@featureflip.io) for Enterprise pricing. Already using LaunchDarkly? [Estimate your LaunchDarkly bill](/tools/launchdarkly-cost-calculator/) against flat pricing, or see [how Featureflip compares to LaunchDarkly](/compare/featureflip-vs-launchdarkly/) on pricing math, feature parity, and migration. ## Frequently asked questions Is Featureflip free to use? Yes. The Free plan costs nothing, does not expire, and is built to run in production. It covers up to 3 team members, 1 project, 10 flags and 2 environments, which is enough to gate features across a staging and production setup. There is no credit card, no trial window and no hidden fee. Team is $49 a month ($39 billed annually) and moves you to 5 team members, 3 projects, 3 environments, unlimited flags, scheduled changes and 30-day audit logs. Pro is $99 a month ($79 annually) for 10 team members, 10 projects, 5 environments and flag owners. Business is $249 a month ($199 annually) and covers 25 team members, 25 projects, unlimited environments, 90-day audit logs and single sign-on through your own identity provider. Start on Free and upgrade when you're ready. Nothing needs migrating. Can I change plans later? Yes, you can upgrade or downgrade your plan at any time from your account settings. Upgrading is instant — as soon as your payment processes, you get immediate access to the new plan's features, higher flag limits, additional projects, and increased team member seats. This means you can start a sprint on the Free plan and move up to Team mid-project without any disruption to your running flags or integrations. Downgrading works on a billing cycle basis: the change takes effect at the end of your current billing period, so you retain full access until then. Your flags and project data are preserved through any plan change — nothing is deleted automatically. If you downgrade below your current usage, you will be prompted to reduce flags or team members before the cycle ends to stay within the new plan limits. What happens if I exceed my plan limits? When you approach your plan's flag or project limits, Featureflip sends you a notification so you have time to act before hitting the ceiling. Existing flag evaluation continues to work without interruption — your users will not see any difference in behavior. The restriction only applies to creating new feature flags or new projects beyond what your plan allows. For example, if you are on the Free plan with its 10-flag limit and try to create an eleventh flag, the creation will be blocked until you either upgrade or delete an existing flag. This design ensures that shipping code is never impacted by billing limits. Upgrading your plan immediately removes the restriction, and you can create new flags right away. We recommend monitoring your usage from the project dashboard to stay ahead of limits before they affect your development workflow. Do you offer annual billing? Yes. Annual billing is available on every paid plan and saves about 20% against paying month by month. Team drops from $49 to $39 a month, which is $120 a year back. Pro drops from $99 to $79 ($240 a year), and Business from $249 to $199 ($600 a year). Annual billing makes sense once feature flags are a settled part of how you ship. You pay for the year upfront and the plan is active for the whole term straight away. You can switch from monthly to annual from your account settings at any time, and the change is prorated, so you only pay for the remaining value of the new term. If your subscription is on an older price, switching moves it to the current one. I subscribed before the current prices. Will my bill change? No. A subscription that started before the current prices keeps the price you signed up at, with everything the plan includes today. It moves to the current price only if you change plans or switch between monthly and annual billing. If you were on the old free Solo plan, you are now on Free, which allows three team members instead of one. You don't need to do anything. What do the paid plans add besides more room? Scheduled changes, cleanup emails, flag owners, and on Business, single sign-on. On Team, Pro and Business you can queue a flag change for a set time, such as turning a feature on for a launch at 9am or ending a promotion at midnight, and Featureflip applies it for you. Those plans also email your admins when a flag passes its expiry date or goes unused. Pro and Business add flag owners, so that cleanup email goes to the person who owns the flag instead of to every admin. When someone leaves, you pick who inherits their flags. Business also lets you sign in through your own identity provider over OIDC, such as Okta or Microsoft Entra ID, and require it for everyone in the organization. Everything else is on every plan, Free included: targeting rules, percentage rollouts, kill switches, all 13 SDKs, the Management API with the MCP server and Terraform provider on top of it, the audit log, two-factor authentication, Google and GitHub sign-in, stale-flag detection, automated flag removal, and flag expiry dates. Past that, the paid plans differ in team members, projects, environments, audit retention and support. Can I manage feature flags as code? Yes, on every plan, Free included. The Featureflip Terraform provider lets you declare projects, environments, flags, variations, segments, and per-environment targeting in .tf files and apply them through your existing pipeline, and it runs on the same public Management API the dashboard uses. There is no paid-tier requirement: you only need to be an organization admin to create the API token it authenticates with. What happens to my flags if Featureflip goes down or shuts down? Day to day, server SDKs hold your configuration in memory and evaluate it inside your own process, so an interruption on our side does not stop your app serving flags, and anything the SDK cannot resolve falls back to the defaults you passed in code. The longer-term worry is whether you can get your configuration out. You can. Every plan, Free included, comes with the public Management API, so you can pull every project, environment, flag, variation, segment, and targeting rule out over REST whenever you like. Going further, the Terraform provider keeps that configuration in your own version control as the source of truth. The SDKs are Apache 2.0 licensed, and the Node.js, .NET, Python, Go, Java and PHP libraries run through OpenFeature, the CNCF vendor-neutral standard, so your call sites stay portable if you ever decide to move. Is there an Enterprise plan? Yes, Featureflip offers Enterprise pricing for organizations that need capabilities beyond what the Business plan provides. Enterprise is a custom arrangement tailored to your team's scale and requirements. Common reasons teams choose Enterprise include needing more than 25 projects, more than 25 team members, custom data retention policies beyond 90-day audit logs, a guaranteed service level agreement, a dedicated support contact, or on-premise deployment for environments where data cannot leave your own infrastructure. Google and GitHub sign-in are available on every plan, including Free, and Business adds single sign-on through your own identity provider over OIDC. Enterprise agreements add SAML/SCIM for enterprise identity providers, along with custom contract terms and volume discounts for large engineering organizations. The pricing is negotiated directly and depends on your usage profile and the specific features you need. To get started, reach out to our sales team at sales@featureflip.io with a description of your team size and requirements, and we will respond within one business day. --- ## Source: https://featureflip.io/compare/ # Compare Featureflip to Other Feature Flag Platforms (2026) [Home](/) / Compare # Compare Featureflip Head-to-head against the major feature flag platforms. Pricing math, feature parity, SDK coverage, and a verdict for each team profile. Each comparison also covers the end of a flag's life. Featureflip's [cleanup Action](/docs/integrations/flag-cleanup-action/) opens a pull request that removes a finished flag from your code, using AST transforms instead of an LLM, and every page shows how the other tool handles that step. Still deciding which vendors belong on your shortlist? Start with the [overview of feature flag tools](/feature-flag-tools/) , which groups the whole category by what each one charges for. Card prices are per month on annual billing. Unleash and GrowthBook publish no annual discount, and PostHog bills by usage, so theirs read the same either way. [## Featureflip vs LaunchDarkly Premium hosted bundle with experimentation, observability, and AI Configs. Featureflip is the focused, flat-rate alternative — no per-MAU surprises and no platform sprawl. LaunchDarkly: $10/service connection past the first 5 + $8.33/1K client MAU · Featureflip: Free, then $39 / $79 / $199/mo flat Read the comparison →](/compare/featureflip-vs-launchdarkly/) [## Featureflip vs Flagsmith Open-source flag platform with hosted SaaS and full self-host. Featureflip is the hosted alternative with flat pricing and unlimited evaluations — no per-API-request metering or 4-seat tier jumps. Flagsmith Start-Up: $40/mo (3 seats, 1M requests) · Featureflip Team: $39/mo (5 seats, unlimited evaluations) Read the comparison →](/compare/featureflip-vs-flagsmith/) [## Featureflip vs ConfigCat Flat-priced flag tool from Hungary with SAML/SCIM on every tier. Featureflip is the cheaper, project-friendlier alternative — 10 projects on Pro vs ConfigCat's 3, no network-traffic metering, native JSON flags. ConfigCat Pro: $110/mo (3 products, 100 flags) · Featureflip Pro: $79/mo (10 projects, unlimited flags) Read the comparison →](/compare/featureflip-vs-configcat/) [## Featureflip vs Unleash Open-source-first flag platform with a per-seat cloud and contact-sales Enterprise. Featureflip is the flat-priced hosted alternative — a free plan, SSE streaming on every plan, and RBAC without an Enterprise contract. Unleash cloud: $75/seat/mo (no free tier) · Featureflip Pro: $79/mo (10 seats, flat) Read the comparison →](/compare/featureflip-vs-unleash/) [## Featureflip vs PostHog All-in-one product suite — analytics, session replay, flags, and experiments — billed by usage. Featureflip is the focused, flat-priced flag tool for teams that already have analytics and want predictable pricing with SSE streaming instead of per-request billing. PostHog: 1M flag requests/mo free, then usage-based · Featureflip Team: $39/mo (flat, unlimited evaluations) Read the comparison →](/compare/featureflip-vs-posthog/) [## Featureflip vs GrowthBook Open-source experimentation suite — flags, A/B testing, and product analytics — with a warehouse-native stats engine and per-seat cloud. Featureflip is the focused, flat-priced flag tool for teams that already measure results in their own analytics and want SSE streaming without running a proxy. GrowthBook Cloud Pro: $40/seat/mo (or free OSS self-host) · Featureflip Team: $39/mo (5 seats, flat) Read the comparison →](/compare/featureflip-vs-growthbook/) ## Looking to switch, not compare? If you've already decided you want to leave your current platform but haven't picked a replacement, the alternatives guides walk through the strongest options for each. [Browse alternatives guides →](/alternatives/) ## Already convinced? Start free on the Free plan — 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/compare/featureflip-vs-configcat/ # ConfigCat Pricing 2026: $110/mo, and the Flat-Rate Case [Home](/) / [Compare](/compare/) / Featureflip vs ConfigCat # Featureflip vs ConfigCat Flat pricing, project ceilings, network-traffic allowances, and SAML/SCIM on free — a 2026 comparison. Last updated: September 14, 2026 **Featureflip is the cheaper, project-friendlier alternative to ConfigCat for small teams.** Both are flat-priced, focused feature-flag tools — no per-seat, no per-MAU fees. On annual billing, Featureflip Team is $39/month for 3 projects and unlimited flags, matching ConfigCat Pro's 3-project ceiling at $110/month for 100 flags. Need more room? Featureflip Pro is $79/month for 10 projects. Pick Featureflip if you run multiple apps or want the cheaper paid tier. Pick ConfigCat if you need Rust, Elixir, Unity, Unreal, or edge-runtime SDKs, or longer audit-log retention. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | ConfigCat | | --- | --- | --- | | Starting price | $0/mo Free (up to 3 team members, 10 flags) | $0/mo Forever Free (unlimited seats, 10 flags, 2 products) | | Paid tier (small team) | $39/mo Team, billed annually ($49 month-to-month) — 3 projects, unlimited flags | $110/mo Pro, billed annually ($120 month-to-month) — 3 projects, 100 flags | | Per-seat fees | — (none) | — (none, unlimited seats) | | SAML / SCIM | Enterprise only (Google and GitHub sign-in on all tiers, OIDC SSO on Business) | All tiers (including Free) | | Network-traffic cap | None — unlimited evaluations | 20 GB / 100 GB / 1 TB / 4 TB per tier, with add-on blocks past it | ## Pricing comparison Both tools are flat-priced, so the comparison reduces to plan ceilings (projects, flags, audit retention) and usage add-ons (paid blocks of extra downloads and traffic for ConfigCat, nothing for Featureflip). ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). Adds self-hosted deployment, SAML/SCIM SSO, custom data retention, dedicated SLA. No per-seat fees. No per-MAU fees. No network-traffic limit. Unlimited evaluations on every plan. Google and GitHub sign-in are included on every plan, including Free. Business adds single sign-on through your own identity provider over OIDC, and Enterprise adds SAML/SCIM. See the [full pricing page](/pricing/) , or read [how flat pricing tiers differ across vendors](/blog/feature-flag-pricing-explained/) before you commit. ### ConfigCat pricing As of September 14, 2026, sourced from [configcat.com/pricing](https://configcat.com/pricing/) . We re-verify quarterly. - **Forever Free** — $0/mo. 2 products, 10 flags, 2 environments, unlimited seats, 20 GB/mo network traffic, 7-day audit log. SSO, SAML, and SCIM included. - **Pro** — $110/mo billed annually ($120 month-to-month). 3 products, 100 flags, 3 environments, unlimited seats, 100 GB/mo network traffic, 35-day audit log. - **Smart** — $325/mo billed annually ($360 month-to-month). 10 products, unlimited flags, unlimited environments, 1 TB/mo network traffic, 35-day audit log. - **Enterprise** — $900/mo billed annually ($1,000 month-to-month). Unlimited products, 4 TB/mo network traffic, 2-year audit log, 99.99% SLA. - **Dedicated** — $4,500/mo, annual billing only. Private cloud, 24 TB/mo network traffic, 2-year audit log. ConfigCat includes SSO, SAML, and SCIM on every tier — a real advantage for procurement-gated buyers. ### Estimate your own ConfigCat bill Enter your product count, team size, and monthly network traffic to estimate ConfigCat's tiered cost against the cheapest Featureflip plan that fits your team and projects. Products (projects)Each ConfigCat "product" — roughly a Featureflip project.Monthly network traffic (GB)Network traffic ConfigCat's SDKs generate downloading your config each month, across all environments. The tier ceilings assume about 4GB per million downloads, but an unchanged config comes back as a small 304 response, so real traffic depends on your config size and how often it changes.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. Billing (applies to both)Annual (billed yearly)Month-to-month ConfigCat Pro $1,320 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on products and monthly network traffic. At this usage, ConfigCat Pro runs about **$1,320/year**, roughly **$852/year more** than Featureflip Team at a flat $468. ConfigCat's bill climbs with your usage. Featureflip's changes only when you move to another plan. ConfigCat rates sourced from [configcat.com/pricing](https://configcat.com/pricing) , checked September 14, 2026. Estimate covers Pro/Smart/Enterprise products and monthly network traffic only; it excludes flags/settings and environment ceilings (fixed per tier, not usage-metered), config JSON download caps (5M / 25M / 250M / 1B a month by tier), which can bind before traffic when configs are small, the annual-only Dedicated plan ($4,500 a month, private cloud or on-premise), which can undercut Enterprise plus add-ons at very high traffic, student/teacher and startup program discounts, negotiated Enterprise/Dedicated terms. Past 1,000 products, ConfigCat raises its technical limits on request, so there is no published price to model. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [ConfigCat cost calculator](/tools/configcat-cost-calculator/) . ### Worked example: small team running 3 apps A team running 3 separate apps (web product, internal admin, mobile), 5 engineers, 12 flags per app (36 flags total): - **Featureflip Team:** $39/mo billed annually = **$468/year**. 3-project ceiling matches this team exactly, with unlimited flags per project and a 30-day audit log. - **ConfigCat Pro:** $110/mo billed annually = **$1,320/year**. Pro caps at 3 products and 100 flags — exactly fits this team, but with no headroom. Adding a 4th app forces **Smart at $325/mo = $3,900/year**. At this profile Featureflip Team is roughly **2.8× cheaper than ConfigCat Pro**. When the 4th app ships, Featureflip moves to Pro at $79/mo annual ($948/year), about 4× under ConfigCat Smart. ### Worked example: solo developer or freelancer - **Featureflip Free:** $0/mo. 1 project, 10 flags, up to 3 team members. - **ConfigCat Forever Free:** $0/mo. **2 products, unlimited seats**, 20 GB/mo network traffic. For freelancers maintaining multiple client projects, ConfigCat's free tier is still more generous on project count. Featureflip Free now covers a small team of up to 3 people rather than a single developer, though it stays capped at 1 project and 10 flags. **Caveat:** ConfigCat includes SAML/SCIM on every tier including free. Featureflip includes Google and GitHub sign-in on every tier and OIDC single sign-on on Business, but requires Enterprise for SAML/SCIM. If your team's procurement requires SAML/SCIM out of the gate, ConfigCat is the cheaper path — until you cross the project or flag ceiling. ### Pricing TL;DR - **Multi-project teams that want the cheapest flat paid tier** — Featureflip Team ($49/mo, 3 projects), or Pro ($99/mo, 10 projects) for more room. - **Solo / freelancer wanting more than 1 project on free** — ConfigCat Forever Free (2 products, unlimited seats). - **Procurement requires SAML/SCIM before purchase** — ConfigCat (SAML/SCIM on every tier, including free). ## Feature comparison Verified against the Featureflip codebase and ConfigCat's public documentation as of September 14, 2026. | Feature | Featureflip | ConfigCat | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ⚠️ (string, int, double — no native JSON) | | Percentage rollouts | ✅ | ✅ | | Real-time updates | ✅ (SSE streaming) | ⚠️ (polling + webhook cache invalidation) | | Local evaluation | ✅ (sub-millisecond) | ✅ (cached after first fetch) | | Kill switches | ✅ | ✅ | | Targeting | | | | User targeting | ✅ | ✅ | | Custom attributes | ✅ | ✅ | | Segments / cohorts | ✅ | ✅ (2 / 3 / unlimited) | | AND/OR condition groups | ✅ | ✅ | | Prerequisite flags | ✅ | ✅ | | Workflow | | | | Multi-environment | ✅ (2 / 3 / 5 / unlimited) | ✅ (2 / 3 / unlimited) | | Audit log | ✅ (7d / 30d / 90d) | ✅ (7d / 35d / 35d / 2y) | | Stale flag detection | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (zombie flags report, last-changed based) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ❌ | | Code references (scan repo for flag usage) | ✅ (cleanup Action, 13 languages) | ✅ (CLI) | | Scheduled changes | ✅ (paid plans) | ✅ (via Change Requests) | | AI agent flag management (MCP server) | ✅ | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP) | ✅ (14 platforms incl. Go, Java, Python, PHP, Ruby, Swift, Rust) | | Webhooks for change events | ✅ (1 / 3 / unlimited) | ✅ (1 / 3 / unlimited) | | Experimentation | | | | A/B variation assignment (multivariate + deterministic hash) | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (use external analytics) | ❌ (use external analytics) | | Enterprise & security | | | | SSO with your identity provider | ✅ (OIDC on Business) | ✅ (all tiers including Free) | | SAML / SCIM | ✅ (Enterprise) | ✅ (all tiers including Free) | | Custom roles / RBAC | ✅ | ✅ (1 / 2 / unlimited groups) | | Self-hosted / private cloud | ✅ (Enterprise) | ✅ (Dedicated, $4,500/mo) | | Pricing model | | | | Per-seat fees | — (none) | — (none, unlimited seats) | | Per-MAU fees | — (none) | — (none) | | Network-traffic cap | — (unlimited) | ✅ (20 GB / 100 GB / 1 TB / 4 TB, soft with add-ons) | | Free tier | ✅ (up to 3 team members, 10 flags) | ✅ (unlimited seats, 10 flags, 2 products) | ## SDK and platform support ConfigCat's SDK breadth is wider. Both tools ship JavaScript, Node.js, Python, .NET, Java, Go, PHP, Ruby, React, Swift/iOS, Android, and Flutter — the major server and client surface. ConfigCat additionally ships Rust, Elixir, Kotlin Multiplatform, Unity, Unreal Engine, Bun, Deno, Cloudflare Workers, and C++. Featureflip's official list covers 13 SDKs; ConfigCat's covers roughly 22. ConfigCat also ships OpenFeature providers for 14 platforms, and Featureflip ships them for Node.js, .NET, Python, Go, Java and PHP. **If your stack uses Elixir, Rust, Kotlin Multiplatform, Unity, Unreal, Bun, Deno, or Cloudflare Workers, ConfigCat is the better fit today.** Game-development teams in particular should default to ConfigCat for the Unity and Unreal SDK coverage. Featureflip's REST and SSE streaming APIs work from any HTTP-capable runtime, but you write the integration yourself. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick ConfigCat when… - **You need SAML or SCIM on day one without paying Enterprise.** ConfigCat ships SAML/SCIM on every tier including the free Forever plan. Featureflip includes Google and GitHub sign-in on every tier and OIDC single sign-on on Business, but reserves SAML/SCIM for Enterprise. For procurement teams that gate vendor approval on SAML/SCIM presence specifically, ConfigCat removes that block at $0/mo. - **Your stack uses Elixir, Rust, Kotlin Multiplatform, Unity, Unreal, Bun, Deno, or Cloudflare Workers.** ConfigCat's SDK list is roughly 1.7× wider and covers game development and edge runtimes that Featureflip does not. - **You need long-term audit-log retention.** ConfigCat Enterprise retains audit logs for 2 years. Featureflip Business is 90 days. - **You're a freelancer or solo dev with multiple client projects.** ConfigCat Forever Free (2 products, unlimited seats) still edges out Featureflip Free (1 project, up to 3 team members) on project count for that profile. ### Pick Featureflip when… - **You want the cheaper paid tier at the same feature surface.** On annual billing, Featureflip Team is $39/mo for 3 projects and unlimited flags, matching ConfigCat Pro's 3-project ceiling at $110/mo for 100 flags. That makes Featureflip roughly 2.8× cheaper at the same usage profile. - **You run multiple apps or expect to.** Featureflip Team covers 3 projects at $39/mo annual, and Pro extends that to 10 projects at $79/mo annual. ConfigCat Pro forces a jump to Smart ($325/mo) the moment you exceed 3 products, a roughly 3× price step against Featureflip's 2×. - **You don't want network-traffic accounting.** Featureflip evaluations are unlimited on every tier. ConfigCat meters network traffic — a non-issue for most apps, but a hidden cost for chatty client-side apps with high MAU. - **You want real-time SSE streaming, not polling.** Featureflip pushes flag changes to SDKs via Server-Sent Events. ConfigCat's default model is polling with webhook-triggered cache invalidation. For kill switches and incident response where seconds matter, SSE is the cleaner model. - **You want finished flags deleted from the code by pull request.** Featureflip marks a flag Stale or Dead from evaluation traffic, and the [cleanup Action](/docs/integrations/flag-cleanup-action/) opens one pull request per flag that removes it, across 13 languages including Swift, Kotlin and Dart. The rewrite uses AST transforms with no LLM, so the same repository always gets the same diff. Merge it, and the flag is archived once the change ships. - **You need native JSON flags.** Featureflip supports JSON variations as a first-class flag type. ConfigCat supports string, integer, and double — JSON encodes as a string but loses SDK call-site type-safety. - **You want a modern, focused tool surface.** No legacy product clutter, no analytics or observability bundled in. The dashboard does one thing. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Migrating from ConfigCat to Featureflip Most teams choose at adoption time rather than migrating. If you're switching, here's what changes. ### Concept mapping The core data model translates cleanly. Most flag definitions move 1:1. | ConfigCat | Featureflip | Notes | | --- | --- | --- | | Product | Project | Same concept, different name. Recreate each ConfigCat product as a Featureflip project. | | User Object | User / context | Context passed per evaluation in both — string, number, boolean attributes. | | Segment | Segment | 1:1 — recreate via the Featureflip dashboard. | | Targeting rule | Targeting rule (condition groups) | Featureflip groups conditions with AND/OR operators between groups; condition operators move 1:1. | | Percentage Option | Percentage rollout | Deterministic-hash bucketing on both sides. | | String / int / double setting | String / number / JSON variation | Int and double both map to Featureflip's number type. ConfigCat JSON-encoded strings move to native JSON variations. | ### Things to plan for - **Prerequisite flags.** ConfigCat's parent-child flag dependencies now migrate 1:1 — Featureflip ships first-class [variation-match prerequisite flags](/blog/prerequisite-flags/) with cycle detection and a dedicated `PrerequisiteFailed` evaluation reason. ConfigCat's "does not equal" comparator inverts to an equivalent "equals" prerequisite during migration. - **Stale flag detection works differently.** ConfigCat's zombie flags report surfaces flags that haven't been changed or toggled within a configured period. Featureflip's [stale flag detection](/docs/concepts/stale-flags/) instead classifies every flag-environment as Active, Stale (30 days), or Dead (90 days) from real evaluation traffic, with reasons attached — so a flag that's still toggled occasionally but serves one variation to everyone is caught too. ConfigCat's code-references CLI has a counterpart as well. The [cleanup Action](/docs/integrations/flag-cleanup-action/) runs in your CI, finds where a finished flag is read, and opens a pull request that deletes it. A key built from a variable at runtime is the exception, and still needs a code search. - **Audit log retention.** Drops from ConfigCat Enterprise's 2 years to Featureflip Business's 90 days. Export ConfigCat logs before migration if you need historical compliance records. - **Webhooks need re-pointing.** ConfigCat's webhook payload shape differs from Featureflip's. Create a [Featureflip webhook subscription](/docs/guides/webhooks/) for each endpoint, then update the receiving code to read the new payload and verify its signature. - **SDK initialisation changes.** Replace `ConfigCatClient.get(sdkKey)` with `FeatureflipClient.get(sdkKey)` at the application entry point. Flag-key strings carry over as-is for most cases. ## Frequently asked questions Is Featureflip cheaper than ConfigCat? At the paid tier, yes. Featureflip Team is $49 per month flat ($39 per month annual) for 3 projects and unlimited flags. ConfigCat Pro is $120 per month ($110 per month annual) for 3 projects and 100 flags. For multi-project teams Featureflip is roughly 2.4× cheaper month-to-month, or 2.8× on annual billing, at the same usage profile. On the free tier ConfigCat is more generous for freelancers with multiple client projects — 2 products and unlimited seats, against Featureflip Free at 1 project and up to 3 team members. Does ConfigCat have a project limit and does Featureflip? Yes to both, and the limits differ significantly at the paid tier. ConfigCat Pro is capped at 3 products. Featureflip Team matches that ceiling at 3 projects for $39 per month on annual billing, and Featureflip Pro goes further to 10 projects for $79 per month annual. The 4th product on ConfigCat forces an upgrade to Smart at $325 per month billed annually. Featureflip has no such cliff between Team and Pro. Does Featureflip charge per network traffic or evaluation like ConfigCat? No. Every Featureflip plan includes unlimited evaluations and has no network-traffic limit. ConfigCat sets a network-traffic allowance per tier (20 GB on Free, 100 GB on Pro, 1 TB on Smart, 4 TB on Enterprise) and sells add-on blocks when you go past it. For most applications the allowance is not binding, but high-MAU client-side apps with chatty SDK polling can hit it. Does Featureflip include SSO and SAML like ConfigCat does on free? Featureflip includes Google and GitHub sign-in on every plan, including Free, and Business adds single sign-on through your own identity provider over OIDC. What requires Enterprise is SAML/SCIM for enterprise identity providers — ConfigCat ships SAML and SCIM on every tier including their free Forever plan. If your procurement process specifically requires SAML/SCIM before purchase, ConfigCat is the cheaper path until you cross the project or flag ceiling. Does Featureflip support multivariate and A/B testing like ConfigCat? Yes. Featureflip supports boolean, string, number, and JSON variations with deterministic-hash percentage rollouts. The bucketing is consistent across sessions and devices, which is the property that makes A/B data analysable. Neither Featureflip nor ConfigCat ships a built-in significance and lift dashboard — both expect you to instrument variation assignment into your own analytics (Mixpanel, Amplitude, your warehouse) and compute results there. LaunchDarkly is a major vendor that bundles the analysis. What SDKs do Featureflip and ConfigCat support? Heavy overlap on the major server and client surface. Both ship JavaScript, Node.js, Python, .NET, Java, Go, PHP, Ruby, React, Swift/iOS, Android, and Flutter. ConfigCat additionally ships Rust, Elixir, Kotlin Multiplatform, Unity, Unreal Engine, Bun, Deno, Cloudflare Workers, and C++. Featureflip additionally ships a dedicated Browser SDK with first-class React bindings. If your stack uses Rust, Elixir, Unity, Unreal, or a modern edge runtime, ConfigCat is the better fit today. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs LaunchDarkly Featureflip's enterprise-tier head-to-head — flat pricing vs LaunchDarkly's bundled platform.](/compare/featureflip-vs-launchdarkly/) [### Featureflip vs Flagsmith The open-source alternative — hosted SaaS vs OSS self-host, flat pricing vs API-request metering.](/compare/featureflip-vs-flagsmith/) [### ConfigCat alternatives guide If you've decided to leave ConfigCat and want a multi-vendor shortlist — six alternatives compared with a decision rubric.](/alternatives/configcat-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All ConfigCat information was sourced from public documentation ([pricing](https://configcat.com/pricing/) , [product](https://configcat.com/) , [SDK reference](https://configcat.com/docs/sdk-reference/overview/) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/compare/featureflip-vs-flagsmith/ # Flagsmith Pricing 2026: What API Metering Actually Costs [Home](/) / [Compare](/compare/) / Featureflip vs Flagsmith # Featureflip vs Flagsmith Flat pricing vs API-request metering, hosted SaaS vs OSS self-host — a 2026 comparison. Last updated: September 14, 2026 **Featureflip and Flagsmith solve the same problem from opposite directions.** Flagsmith is open-source (BSD-3-Clause) with a free self-hosted edition and a usage-metered SaaS that charges per million API requests. Featureflip is a closed-source SaaS with flat monthly pricing and unlimited evaluations. Pick Flagsmith if you want to self-host or run on the open-source edition. Pick Featureflip if you want predictable flat billing for a hosted product without API-request accounting and without operating your own infrastructure. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | Flagsmith | | --- | --- | --- | | Starting price | $0/mo Free (up to 3 team members, 10 flags) | $0/mo SaaS (1 user, 50K req/mo) or free OSS self-host | | Paid tier (small team) | $39/mo annual Team — 5 users, unlimited flags & evaluations | $40/mo annual Start-Up — 3 users, 1M API req/mo | | Open source | No (closed-source SaaS) | Yes (BSD-3-Clause) | | Self-hostable | Enterprise only | Yes, free (community edition) | | Pricing model | Flat — unlimited evaluations | Metered: from $50 per additional million API requests, falling with volume | ## Pricing comparison The biggest practical differences between Featureflip and Flagsmith are the pricing model (flat vs metered) and the hosting model (SaaS-only vs SaaS-or-OSS). ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). Adds self-hosted deployment, SAML/SCIM SSO, custom data retention, dedicated SLA. No per-seat fees. No per-API-request fees. Unlimited evaluations on every plan. Google and GitHub sign-in are included on every plan, including Free. Business adds single sign-on through your own identity provider over OIDC, and Enterprise adds SAML/SCIM. See the [full pricing page](/pricing/) , or read [the case for flat pricing over per-seat plans](/blog/feature-flag-pricing-explained/) as your team grows. ### Flagsmith pricing As of September 14, 2026, sourced from [flagsmith.com/pricing](https://www.flagsmith.com/pricing) . We re-verify quarterly. - **Free** — $0/mo SaaS. 50K API requests/month, 1 team member, unlimited flags/environments/identities. Overage $50 per million. *Or self-host the BSD-3-Clause community edition for $0 in license fees.* - **Start-Up** — $40/mo annual ($45 monthly). 1M API requests/month, 3 team members, unlimited projects, scheduled flags, 2FA, A/B/MVT testing, third-party integrations. 14-day trial. Overage starts at $50/M and falls with volume. - **Scale-Up** — $250/mo annual ($300 monthly). 5M API requests/month, 5 team members included, additional members at $50/seat/month on annual billing or $60 month-to-month (up to 20 total), SAML/SSO, RBAC, change requests, audit logs, priority support. Overage starts at $50/M and falls with volume. - **Enterprise** — Custom. LDAP/SCIM, dedicated SLA, real-time chat support, on-prem and air-gapped hosting. ### Estimate your own Flagsmith bill Enter your monthly API requests and team size to estimate Flagsmith's metered SaaS cost against Featureflip's flat price. Defaults to annual billing; switch to month-to-month with the toggle. Monthly API requestsTotal flag/identity evaluation calls Flagsmith counts each month, across all environments.Team membersEveryone with access to your Flagsmith organisation. Billing (applies to both)Annual (billed yearly)Month-to-month Flagsmith Scale-Up $3,000 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on API requests and team members. At this usage, Flagsmith Scale-Up runs about **$3,000/year**, roughly **$2,532/year more** than Featureflip Team at a flat $468. Flagsmith's bill climbs with your usage. Featureflip's changes only when you move to another plan. Flagsmith rates sourced from [flagsmith.com/pricing](https://www.flagsmith.com/pricing) , checked September 14, 2026. Estimate covers Start-Up/Scale-Up API requests and team members only; it excludes self-hosted / on-prem deployment (free license, but you carry the operational cost), negotiated Enterprise terms. Enterprise (more than 20 seats, LDAP or SCIM, or custom hosting) is quote-only. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [Flagsmith cost calculator](/tools/flagsmith-cost-calculator/) . ### Worked example: small team, 50K-MAU SaaS, 8 engineers - **Featureflip Pro:** $79/mo annual = **$948/year**, 10 seats included, unlimited evaluations. - **Flagsmith Start-Up doesn't fit:** only 3 seats included — 8 engineers forces Scale-Up at $250/mo annual = $3,000/year base, plus 3 additional seats at $50/seat/mo = +$1,800/year. Total **$4,800/year**. For a hosted small team at this profile, Featureflip Pro is roughly **5× cheaper**. ### Worked example: tiny team, 1–3 engineers - **Featureflip Free:** $0/mo, forever, for up to 3 team members. Caps at 1 project and 10 flags per project. - **Flagsmith Start-Up annual:** $40/mo = **$480/year**, 3 seats included. A team of up to 3 people fits entirely on Featureflip's Free plan, so the whole $480/year gap is avoided. If you outgrow the 1-project or 10-flag ceiling, Featureflip Team is $39/mo annual (**$468/year**) for up to 5 seats and 3 projects, still cheaper than Flagsmith Start-Up, with two spare seats and no Scale-Up cliff at the fourth hire. ### Worked example: high-traffic backend A backend service evaluating flags 100 million times per month (mobile API, ad-serving path, IoT fleet): - **Featureflip Pro or Business:** $99–249/mo flat. Unlimited evaluations. - **Flagsmith Scale-Up:** 5M included, 95M overage at the $50/M starting rate = up to **+$4,750/mo** on top of plan. For high-traffic API surfaces, the Featureflip flat-rate model compounds in your favour every month. **Caveat:** if you run Flagsmith's open-source community edition yourself, license fees are $0 — the trade-off is operational ownership (DB hosting, version upgrades, on-call). The fair cost comparison there is hosting + engineering hours, not list price. ### Pricing TL;DR - Want a **free flag platform you'll operate yourself** — Flagsmith OSS. - Want **hosted SaaS with flat pricing and unlimited evaluations**: Featureflip is cheaper at every team size on annual billing, and month-to-month once you pass 3 seats or 1M requests. - Run a **high-traffic backend** where API-request metering becomes painful — Featureflip's flat plan removes the variable. ## Feature comparison Verified against the Featureflip codebase and Flagsmith's public documentation as of September 14, 2026. | Feature | Featureflip | Flagsmith | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ✅ | | Percentage rollouts | ✅ | ✅ | | Real-time updates | ✅ (SSE streaming) | ✅ (real-time flags, Enterprise) | | Sub-millisecond local evaluation | ✅ | ✅ | | Edge / global low-latency runtime | ❌ | ✅ (Edge API) | | Kill switches | ✅ | ✅ | | Targeting | | | | User targeting | ✅ | ✅ (identities) | | Custom attributes / traits | ✅ | ✅ | | Segments / cohorts | ✅ | ✅ | | AND/OR condition groups | ✅ | ✅ | | Server-side stored identity / trait API | ❌ | ✅ | | Workflow | | | | Multi-environment | ✅ | ✅ | | Audit log | ✅ (7d / 30d / 90d by plan) | ✅ (Scale-Up+) | | Stale flag detection | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (Scale-Up+, time-based: no changes for 30 days by default) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ❌ | | Scheduled changes | ✅ (paid plans) | ✅ (Start-Up+) | | Approval workflows / change requests | ❌ | ✅ (Scale-Up+) | | Code references (auto-detect flag usage in repo) | ✅ (cleanup Action, 13 languages) | ✅ (GitHub Actions) | | AI agent flag management (MCP server) | ✅ | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP) | ✅ (Go, Java, .NET, JavaScript web + server, Kotlin, Python, Ruby, Rust, Swift) | | 2FA | ✅ | ✅ (Start-Up+) | | Experimentation | | | | A/B / multivariate variation assignment | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (use external analytics) | ✅ (Enterprise beta) | | Open source & hosting | | | | Open-source license | ❌ | ✅ (BSD-3-Clause) | | Free self-hosted edition | ❌ | ✅ | | Hosted SaaS | ✅ | ✅ | | Air-gapped / on-prem | ✅ (Enterprise) | ✅ (Enterprise) | | Enterprise | | | | SSO with your identity provider | ✅ (OIDC on Business, SAML/SCIM on Enterprise) | ✅ (Scale-Up+) | | SAML / SCIM | ✅ (Enterprise) | ✅ (Scale-Up / Enterprise) | | Custom roles / RBAC | ✅ | ✅ (Scale-Up+) | | Pricing | | | | Per-seat fees | — (none) | ✅ ($50/seat annual or $60 monthly over Scale-Up cap) | | Per-API-request fees | — (none) | ✅ (overage from $50/M, falls with volume) | | Free SaaS tier | ✅ (Free, up to 3 people, 10 flags) | ✅ (50K req/mo) | | Free self-host tier | ❌ | ✅ | ## SDK and platform support The two products overlap heavily on the major server and client surface. Both ship JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, and Flutter SDKs. Featureflip adds a dedicated Browser SDK. Flagsmith adds Rust, Elixir, React Native, and Next.js, plus an Edge API that routes requests to the closest of eight data centers. **If your stack uses Rust or you need a global edge runtime today, Flagsmith is the better fit.** For everything else, both work. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick Flagsmith when… - **You want to self-host for free.** BSD-3-Clause license and no fees for core flagging. Audit logs, RBAC, SAML/SSO, and change requests are proprietary and ship in the Enterprise image. The right choice if you have a platform team and prefer running infrastructure to paying for it. - **You need an edge runtime.** Flagsmith's Edge API serves flags from eight regions and routes each request to the closest one. - **You need approval workflows or change requests out of the box.** Both are still on the Featureflip roadmap. - **Your stack uses Rust.** Flagsmith ships a Rust SDK; Featureflip does not. - **You're already standardised on Flagsmith's identity API model** (server-side stored traits) and don't want to refactor. ### Pick Featureflip when… - **You want flat pricing with unlimited evaluations.** No API-request metering, no overage surprises, no forecasting evaluation budgets. The plan number is the bill. - **You're a high-traffic SaaS or mobile app.** Flagsmith's overage starts at $50 per million requests, so the bill grows with your traffic while Featureflip's flat fee stays put. On annual billing Featureflip is cheaper at any volume, and month-to-month it pulls ahead once you outgrow Start-Up's 1M requests or 3 seats. - **You're on hosted SaaS at any team size.** Featureflip Team is $39/mo annual for up to 5 seats, already ahead of Flagsmith Start-Up's 3. Need more room? Pro is $79/mo annual for up to 10 seats, still far under Flagsmith's $250/mo Scale-Up at the fourth hire. - **You don't want to operate flag infrastructure.** No DB hosting, version upgrades, monitoring, or on-call rotation. Hosted is the trade-off. - **You want a focused flag UI.** No identity stores, no trait management, no remote-config layer to learn first. Flags, segments, environments — that's it. - **You want flag debt to surface itself.** Featureflip automatically marks flags [Stale or Dead](/docs/concepts/stale-flags/) from real evaluation traffic, with reasons per environment. Flagsmith's Stale Flag Detection (Scale-Up and above) is time-based: by default it marks any flag that has gone 30 days without a change in any environment. Once a flag is finished, the [cleanup Action](/docs/integrations/flag-cleanup-action/) opens a pull request that removes it from your code in any of 13 languages, then archives the flag after the change ships. It rewrites with AST transforms instead of an LLM, so a rerun gives you the same diff. - **You want predictable annual budgeting.** The flat fee is forecastable a year out without modelling traffic growth. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Migrating from Flagsmith to Featureflip Most teams choose at adoption time rather than migrating. If you're switching, here's what changes. ### Concept mapping The core data model translates cleanly. Most flag definitions move 1:1. | Flagsmith | Featureflip | Notes | | --- | --- | --- | | Identity | User / context | Featureflip context is passed per evaluation, not stored server-side | | Trait | Custom attribute | Same shape — string, number, boolean values on the user context | | Segment | Segment | 1:1 — recreate via the Featureflip dashboard | | Multivariate flag | Multivariate flag | Boolean, string, number, or JSON variations on both sides | | Percentage rollout | Percentage rollout | Deterministic-hash bucketing on both sides | ### Things to plan for - **Identity API → per-call context.** Flagsmith stores user traits server-side via the Identity API. Featureflip SDKs pass context with each evaluation. Expect a small refactor at the SDK call site, not a rewrite. - **Approval workflows / change requests.** Roadmap on Featureflip. Until those ship, dashboard-based RBAC and the audit log are the available controls — restrict who can edit production flags and review changes after the fact via the audit log. - **Self-hosted Flagsmith cost math.** Compare your current spend (DB hosting, container runtime, CI for upgrades, on-call hours) against Featureflip's monthly fee. Hosted wins when the operational cost exceeds the plan price. ## Frequently asked questions Is Flagsmith open source and Featureflip not? Yes. Flagsmith's core is licensed BSD-3-Clause and free to self-host on Docker, Kubernetes, or OpenShift. Audit logs, role-based access control, SAML/SSO, and change requests are proprietary and ship in Flagsmith's Enterprise image. Featureflip is a closed-source SaaS; self-hosting is available only on the Enterprise plan. If running your own flag infrastructure for $0 in license fees is the requirement, Flagsmith is the right pick. If you would rather pay a flat monthly fee than operate the database, application, and on-call rotation yourself, Featureflip is built for that trade-off. Is Flagsmith cheaper than Featureflip? Only if you self-host the open-source edition, or pay month-to-month for up to 3 seats and 1 million API requests. On annual billing, Featureflip is cheaper at every team size. Featureflip Team is $39 per month annual ($49 monthly) with 5 seats included, and Flagsmith Start-Up is $40 per month annual ($45 monthly) with 3 seats included. Month-to-month, that leaves Start-Up $4 a month cheaper than Team. The 4th hire forces Flagsmith Scale-Up at $250 per month annual. Featureflip Team at $39 stays flat through 5 seats, and a team that grows past that moves to Pro at $79 per month annual for up to 10 seats. For high-traffic apps the gap widens because Featureflip evaluations are unlimited while Flagsmith bills requests over plan at a rate that starts at $50 per million and falls with volume. Does Featureflip charge per API request or evaluation like Flagsmith? No. Every Featureflip plan includes unlimited evaluations. Flagsmith meters API requests: 50,000 per month free, 1 million on Start-Up, 5 million on Scale-Up, with overage on every paid tier starting at $50 per additional million and falling with volume. For a backend service evaluating flags 100 million times per month, Flagsmith would add up to $4,750 per month in overage on top of plan at that $50 starting rate. Featureflip stays at $49–249 per month flat. Can I self-host Featureflip like Flagsmith? Self-hosting is available on Featureflip Enterprise. It is not part of the free or Pro tiers. Flagsmith's free BSD-3-Clause self-host is the cheaper path if running your own flag platform is non-negotiable — there is no license fee and no seat cap on the community edition. The trade-off is operational ownership: database hosting, version upgrades, monitoring, and the on-call cost when something breaks. If those costs exceed Featureflip's monthly fee for your team, hosted is the better fit. Does Featureflip support multivariate and A/B testing like Flagsmith? Yes — Featureflip supports boolean, string, number, and JSON variations with deterministic-hash percentage rollouts. The bucketing is consistent across sessions and devices, which is the property that makes A/B data analysable. Featureflip expects you to instrument variation assignment into your own analytics (Mixpanel, Amplitude, your warehouse) and compute significance and lift there. Flagsmith has built-in experiment analysis in beta on Enterprise plans. It needs a connected data warehouse and reports lift, credible intervals, and win probability. LaunchDarkly is a major vendor that bundles the analysis. What SDKs do Featureflip and Flagsmith support? Heavy overlap on the major server and client surface. Both ship JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, and Flutter. Flagsmith adds Rust, Elixir, React Native, and Next.js, plus an Edge API that routes requests to the closest of eight data centers. Featureflip adds a dedicated Browser SDK. If your stack uses Rust today or you need an edge runtime for client-side flags worldwide, Flagsmith is the better fit. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs LaunchDarkly Featureflip's other major head-to-head — flat pricing vs LaunchDarkly's bundled platform.](/compare/featureflip-vs-launchdarkly/) [### Flagsmith alternatives guide If you're not yet sold on Featureflip — a broader six-vendor comparison including Unleash, ConfigCat, GrowthBook, and PostHog.](/alternatives/flagsmith-alternative/) [### Featureflip vs ConfigCat Both flat-priced, no per-MAU fees. The wedge is project ceilings and SAML/SCIM availability — ConfigCat ships SAML/SCIM on free, Featureflip is cheaper per project.](/compare/featureflip-vs-configcat/) [### Best LaunchDarkly alternatives Leaving LaunchDarkly rather than Flagsmith? A six-vendor guide with a cost-at-scale pricing table and migration notes.](/alternatives/launchdarkly-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All Flagsmith information was sourced from public documentation ([pricing](https://www.flagsmith.com/pricing) , [product](https://www.flagsmith.com/) , [SDK reference](https://docs.flagsmith.com/) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/compare/featureflip-vs-growthbook/ # GrowthBook Pricing 2026: $40 a Seat vs a Flat $39 [Home](/) / [Compare](/compare/) / Featureflip vs GrowthBook # Featureflip vs GrowthBook A focused flag platform with flat pricing and SSE streaming vs an open-source, warehouse-native experimentation suite. A 2026 comparison. Last updated: September 14, 2026 **Featureflip and GrowthBook both manage feature flags, but they solve different problems.** GrowthBook is an open-source experimentation platform — feature flags, A/B testing, and product analytics — built around a warehouse-native statistics engine that computes results on your own data. Featureflip is a focused feature flag platform with flat monthly pricing, real-time SSE streaming, and a free plan. Pick GrowthBook if experimentation is central and you want a rigorous, free stats engine running on your own warehouse. Pick Featureflip if you want a focused flag tool with predictable flat pricing and streaming, and you already measure results in your own analytics. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | GrowthBook | | --- | --- | --- | | Starting price | $0/mo Free (cloud, free forever) | $0 — Cloud Starter (≤3 users) or free OSS self-host | | Paid model | $39/mo annual Team — flat, 5 users, unlimited evaluations | $40/seat/mo Cloud Pro, one seat per user | | Product scope | Focused feature flag platform | Experimentation suite — flags + experiments + product analytics, warehouse-native | | Built-in experiment analysis | No (delivery layer — bring your own analytics) | Yes (Bayesian + frequentist stats engine) | | Real-time updates | SSE streaming, all plans, no proxy | SSE on Cloud, or self-host + run the GrowthBook Proxy | ## Pricing comparison The biggest practical difference is the pricing model: Featureflip charges a flat monthly fee with unlimited evaluations, while GrowthBook Cloud charges per user seat, or is free if you self-host the open-source edition and run the operations yourself. Which one is cheaper depends almost entirely on whether you want hosted flags for a team or a self-operated experimentation stack. ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). Adds self-hosted deployment, SAML/SCIM SSO, custom data retention, dedicated SLA. No per-seat fees. No per-request fees. Unlimited evaluations on every plan. Google and GitHub sign-in are included on every plan, including Free. Business adds single sign-on through your own identity provider over OIDC, and Enterprise adds SAML/SCIM. See the [full pricing page](/pricing/) , or read why [flat pricing beats per-seat billing](/blog/feature-flag-pricing-explained/) as your team grows. ### GrowthBook pricing As of September 14, 2026, sourced from [growthbook.io/pricing](https://www.growthbook.io/pricing) . We re-verify quarterly. - **Starter (Cloud)** — $0, no credit card. Up to **3 users** on 1 project, unlimited flags and experiments, a managed warehouse, and CDN limits (1M requests, 5GB bandwidth). - **Pro (Cloud)** — **$40/seat/month**, up to 50 users and 3 projects. Adds advanced statistics (CUPED, sequential testing, multi-arm bandits), the visual A/B editor, sticky bucketing, safe rollouts, Product Analytics, and a 2M-events/month warehouse. - **Enterprise (Cloud)** — custom, contact sales. Adds OIDC/SAML SSO, custom roles, data pipelines, advanced prerequisites, approval flows, exportable audit logs, and a 99.99% SLA. - **Open Source (self-hosted)** — $0, unlimited users on 1 project. Unlimited flags, experiments, and traffic, with warehouse-native support and community support. You operate the app, the proxy, and your warehouse. ### Estimate your own GrowthBook bill Enter how many people need a GrowthBook login to see what Cloud costs at $40 a seat, against Featureflip's flat price. The first three seats are free on Starter. Team members (seats)Everyone who needs a GrowthBook login, since GrowthBook counts every user of the platform as a seat. End users of your app are not counted. The first 3 are free on Starter. Add a 4th and the whole team moves to Pro, where every seat costs $40 a month. GrowthBook Pro (Cloud) $2,400 per year, estimated Featureflip Team $468 per year, flat. No usage-based fees on Cloud seats. At this usage, GrowthBook Pro (Cloud) runs about **$2,400/year**, roughly **$1,932/year more** than Featureflip Team at a flat $468. GrowthBook's bill climbs with your usage. Featureflip's changes only when you move to another plan. GrowthBook rates sourced from [growthbook.io/pricing](https://www.growthbook.io/pricing) , checked September 14, 2026. Estimate covers Pro (Cloud) Cloud seats only; it excludes the free self-hosted open-source edition (no licence fee but you run the app, the proxy, and your warehouse), CDN overage on Pro ($10 per million requests past 2 million, $1 per GB past 20GB), managed-warehouse events past the first 2 million ($0.03 per 1,000), quote-only Enterprise terms (SAML or OIDC SSO, SCIM, custom roles, approval flows, a 99.99% SLA). Above 50 users, GrowthBook Cloud moves to quote-only Enterprise, so this estimate stops rather than extrapolating the seat rate. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [GrowthBook cost calculator](/tools/growthbook-cost-calculator/) . ### Worked example: a growing team that just wants flags (hosted) - **GrowthBook Cloud Pro:** 5 editors × $40/seat = **$200/month**, climbing $40 with every editor you add. - **Featureflip Team:** **$39/month** annual, flat, for up to 5 users and unlimited evaluations. For a flag-only team on a hosted plan, Featureflip Team is roughly **5× cheaper** at five seats, and it doesn’t charge per editor up to that ceiling. Need more room? Pro is $79/month annual for up to 10 users. GrowthBook’s free Starter covers up to three users, so the cost only really shows up once you outgrow it. ### Worked example: self-hosting for free - **GrowthBook OSS:** $0 in licence — unlimited users. You run the application, the GrowthBook Proxy (for SSE streaming and caching), and ideally connect a data warehouse for experiment analysis. - **Featureflip:** Free is $0 hosted with zero ops, or Team at $39/month annual for up to 5 seats. Supported self-hosting is available on Enterprise. If you want a free, fully owned stack and have the operational appetite, GrowthBook’s open-source edition is unbeatable on licence cost. You trade subscription spend for the work of running the app, the proxy, and a warehouse. ### Worked example: a serious experimentation program with a warehouse - **GrowthBook:** the statistics engine (Bayesian + frequentist, CUPED, sequential testing, bandits) runs directly on your Snowflake, BigQuery, Redshift, or Databricks data — comparable capability elsewhere costs thousands. - **Featureflip:** assigns variations and emits exposure events, then hands measurement to your analytics or warehouse. It doesn’t compute significance. This is the profile where GrowthBook is genuinely the better tool, and Featureflip doesn’t try to match it. If experiments are central and your data already lives in a warehouse, GrowthBook’s analysis is the draw. **Honest caveat:** GrowthBook's per-seat fee counts everyone with a GrowthBook login (your app's end users aren't counted), and the free Starter (3 users) plus the free open-source edition make it cheap for tiny teams and self-hosters. The cost pressure appears as your team grows on Cloud, and the operational burden appears on self-host. Featureflip’s flat fee removes the per-seat math but doesn’t include an experiment-analysis engine — that’s the trade you’re weighing. ### Pricing TL;DR - Want a **free, rigorous experiment-analysis engine** on your own warehouse — GrowthBook is excellent value, and Featureflip doesn’t try to match it. - Just want **hosted feature flags for a growing team** — Featureflip’s flat fee beats per-seat pricing that climbs with every editor. - Want **hosted flags with zero ops** — Featureflip needs no app, proxy, or warehouse to run; GrowthBook self-host does. ## Feature comparison Verified against the Featureflip codebase and GrowthBook’s public documentation as of September 14, 2026. GrowthBook is the broader product; the rows that matter are scope, pricing model, experimentation depth, and propagation, not raw feature count. | Feature | Featureflip | GrowthBook | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ✅ | | Percentage rollouts | ✅ | ✅ | | Local SDK evaluation | ✅ (sub-millisecond) | ⚠️ (local eval; no published latency figure) | | Real-time updates | ✅ (SSE streaming, all plans) | ⚠️ (SSE on Cloud or via the proxy) | | Kill switches | ✅ | ✅ | | Flag prerequisites / dependencies | ✅ | ⚠️ (Pro and up, advanced on Enterprise) | | Remote config (JSON payloads) | ✅ (JSON variations) | ✅ | | Targeting | | | | User targeting | ✅ | ✅ | | Custom attributes / context fields | ✅ | ✅ (targeting attributes) | | Segments / saved groups | ✅ | ✅ (saved groups) | | AND/OR condition groups | ✅ | ✅ | | Workflow | | | | Multi-environment | ✅ (2 / 3 / 5 / unlimited by plan) | ✅ | | Audit log | ✅ (7d / 30d / 90d by plan) | ⚠️ (exportable logs Enterprise-only) | | Scheduled / ramp changes | ✅ (paid plans) | ✅ (ramp schedules Enterprise, scheduled flags from Pro) | | Stale-flag detection / lifecycle | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (automatic stale detection on all plans, code references from Pro) | | AI agent flag management (MCP server) | ✅ | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP) | ✅ (Java, Python, Go, .NET, JavaScript for Node.js and web) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ⚠️ (via MCP and your AI agent) | | Experimentation & analytics | | | | A/B / multivariate variation assignment | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (use external analytics) | ✅ (Bayesian + frequentist) | | Advanced stats (CUPED, sequential, bandits) | ❌ | ✅ (Pro) | | Warehouse-native analysis | ❌ | ✅ (BigQuery, Snowflake, Redshift…) | | Product analytics | ❌ (by design — bring your own) | ✅ (Pro) | | Exposure / experiment events | ✅ (events API) | ✅ (native) | | Open source & hosting | | | | Open-source license | ❌ | ✅ (MIT core; some Enterprise dirs) | | Self-hosting | ✅ (Enterprise, supported) | ✅ (free OSS, self-operated) | | Hosted SaaS / cloud | ✅ | ✅ (GrowthBook Cloud) | | Zero-ops hosted (no proxy / warehouse to run) | ✅ | ✅ (streaming and a managed warehouse on every Cloud plan) | | Enterprise | | | | Google sign-in | ✅ (plus GitHub, every plan including Free) | ✅ (Starter and Pro) | | SSO with your identity provider / SAML | ✅ (OIDC on Business, SAML/SCIM on Enterprise) | ✅ (OIDC/SAML and SCIM on Enterprise) | | Custom roles / RBAC | ✅ | ✅ (advanced permissioning Pro and up, custom roles Enterprise) | | Pricing | | | | Pricing model | Flat (unlimited evaluations) | Per-seat (Cloud) or free OSS | | Free hosted / cloud tier | ✅ (Free, up to 3 people, 10 flags) | ✅ (Starter, ≤3 users) | | Per-seat billing | — (none) | ✅ ($40/seat/mo on Cloud) | | Predictable flat cost | ✅ | ⚠️ (flat only when self-hosting) | ## SDK and platform support Both products cover the mainstream server and client surface. Featureflip ships JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, Flutter, a dedicated framework-agnostic Browser SDK, and OpenFeature providers for Node.js, .NET, Python, Go, Java and PHP. GrowthBook has the broader official catalog. Alongside the mainstream languages it adds Elixir, Rust, React Native, and edge SDKs for Cloudflare Workers, Fastly, and Lambda@Edge. Its OpenFeature providers cover five of those six languages (all but PHP), plus a browser provider. Both evaluate flags locally in backend SDKs, so the server-side runtime model is similar. **If you need edge SDKs, Elixir, or Rust today, GrowthBook has the wider coverage.** For the mainstream server, React, and mobile surface — with OpenFeature providers for Node.js, .NET, Python, Go, Java and PHP and SSE streaming on every plan without standing up a proxy — Featureflip is the simpler fit. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick GrowthBook when… - **Experimentation is central.** You run A/B tests regularly and want a rigorous statistics engine that does more than assign variations. Bayesian and frequentist analysis come with every plan, free ones included, and CUPED, sequential testing, and multi-arm bandits are on Cloud Pro (Enterprise when self-hosted). - **You want warehouse-native analysis.** GrowthBook computes results directly on your Snowflake, BigQuery, Redshift, or Databricks data, so you keep data ownership and don’t ship events to a third party. - **You want open source you can self-host.** An MIT-core codebase, unlimited self-hosted seats, and an active community fit teams that want to own the stack. - **You want flags, experiments, and product analytics in one tool.** The bundle is the point for an experimentation-led org. - **You already run a data warehouse.** That’s where GrowthBook’s differentiator lives, and where it’s strongest. ### Pick Featureflip when… - **You want flags, not an experimentation platform.** A focused tool with a cleaner mental model and no stats engine, metrics, or warehouse to learn and govern. - **You want flat, predictable pricing.** No per-seat fee that climbs as your editor team grows — unlimited evaluations on every plan. - **You want real-time SSE streaming with nothing to operate.** No GrowthBook Proxy to deploy; streaming is on by default on every plan. - **You want flag cleanup to run unattended.** Featureflip's [cleanup Action](/docs/integrations/flag-cleanup-action/) runs on your schedule in CI and opens a pull request for each finished flag, in any of 13 languages. It uses AST transforms and no LLM, so the diff you review is the one you would get on a rerun. The flag is archived once the removal ships. - **You already measure results in your analytics.** If experiments land in Amplitude, Mixpanel, GA4, or your warehouse, you don’t need a second platform just to flip flags. - **You want hosted flags with zero ops.** No app, proxy, or warehouse to run — Free is free forever, and supported self-hosting is available on Enterprise. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Can you use GrowthBook and Featureflip together? Yes, and it’s a natural split. They overlap only on flag delivery; GrowthBook’s analysis and Featureflip’s delivery are complementary. Keep GrowthBook for warehouse-native experiment analysis. Use Featureflip as the flag-delivery layer — flat pricing, SSE streaming, and sub-millisecond local evaluation, with no proxy to operate. Featureflip assigns variations and emits exposure events through its events API; route those into your warehouse and let GrowthBook compute significance on them. You get GrowthBook’s statistics on your own data without running GrowthBook’s flag-delivery and proxy infrastructure. ### Moving flags off GrowthBook: concept mapping If you’re consolidating flag management onto Featureflip, the core model translates cleanly. | GrowthBook | Featureflip | Notes | | --- | --- | --- | | Feature (boolean / multivariate) | Feature flag with boolean or multivariate variations | Boolean, string, number, or JSON variations on both sides | | Attribute targeting / condition | Targeting rule / condition group | Re-express attribute conditions as rules with AND/OR groups | | Saved group | Segment | Static saved groups map directly to reusable segments | | Rollout / namespace | Percentage rollout | Deterministic-hash bucketing with sticky assignment on both sides | | Experiment | Variation assignment + exposure events | Analysis stays in GrowthBook or your warehouse; Featureflip delivers the split | | Remote config value | JSON variation | Serve structured config as a JSON variation value | ### Things to plan for - **Self-host plus proxy to hosted SSE.** If you ran the GrowthBook app and the Proxy for streaming, Featureflip removes both — streaming is hosted and on by default. Expect a small change at SDK init, not a rewrite. - **Experiment analysis stays where your data lives.** Featureflip assigns variations and emits exposure events; it doesn’t compute significance. Keep GrowthBook (or your warehouse and analytics) for the statistical readout. - **Per-seat to flat.** Cloud GrowthBook bills a seat for every user, while Featureflip charges one flat price per plan. Your budgeting shifts from “per person” to “per tier.” - **Attribute model.** Both pass user attributes to the SDK at evaluation time, so targeting translates cleanly — just re-declare the attributes you target on. - **Scripted flag changes port over.** If you script flag changes against GrowthBook’s API, Featureflip’s [Management API](/product/features/api/) covers the same ground: flag CRUD, per-environment toggles, targeting rules, and segments over REST, with idempotent creates and every change recorded in the audit log. ## Frequently asked questions Is GrowthBook free? Yes, in two ways. GrowthBook Cloud has a free Starter plan for up to 3 users with unlimited flags and experiments and a managed warehouse. Separately, the open-source edition is free to self-host with unlimited users — its core is MIT-licensed, with a few Enterprise-only directories. Paid GrowthBook Cloud is the Pro plan at $40 per seat per month (up to 50 users), and Enterprise is contact-sales. Governance features like OIDC/SAML SSO, exportable audit logs, and approval flows are Enterprise-only. Is Featureflip cheaper than GrowthBook? It depends on how you run it. For a growing team that just wants feature flags on a hosted plan, Featureflip is usually cheaper: Team is a flat $39/month annual for up to 5 users with unlimited evaluations, while GrowthBook Cloud Pro is $40 per seat per month — so a five-editor team is roughly $200/month, and the gap widens as you add seats. For a team that wants a free open-source experimentation stack and is willing to run the operations, GrowthBook’s self-hosted edition is unbeatable on license cost — you pay in ops and infrastructure instead of subscription. Should I use GrowthBook or Featureflip? If experimentation is central to how you work — you run A/B tests regularly and want a rigorous statistics engine computing results on your own data warehouse — GrowthBook is the stronger fit. If you want a focused flag tool with flat, predictable pricing and real-time SSE streaming, and you already measure results in your existing analytics, Featureflip is the better fit. The deciding question is usually whether you want to adopt an experimentation platform (and ideally a data warehouse) at all just to manage flags. Does GrowthBook do A/B testing and experiment analysis that Featureflip doesn’t? Yes — this is GrowthBook’s core strength. It ships a warehouse-native statistics engine with both Bayesian and frequentist methods, plus CUPED variance reduction, sequential testing, multi-arm bandits, and sample-ratio-mismatch checks, computed directly on your Snowflake, BigQuery, Redshift, or Databricks data. Featureflip handles the delivery side of experiments — sticky variation assignment and exposure events through its events API — and leaves the statistical measurement to your analytics stack or warehouse. If you want the analysis built in, pick GrowthBook. If you already measure results elsewhere, Featureflip plugs into that flow without a second platform to learn. Does Featureflip stream flag updates in real time like GrowthBook? Featureflip pushes flag changes over Server-Sent Events (SSE) on every plan, with nothing extra to run, so SDKs update in near real time without polling. GrowthBook supports SSE streaming too, but only on GrowthBook Cloud or if you deploy and operate the self-hosted GrowthBook Proxy — a plain self-host without the proxy does not stream. If you want real-time flag updates without standing up a proxy, Featureflip is simpler. Is GrowthBook open source and self-hostable? Yes. GrowthBook's core is MIT-licensed with a few Enterprise-only directories, and you can self-host the open-source edition for free with unlimited users. That edition is limited to 1 project and leaves out Enterprise features such as CUPED, bandits, SSO, and approval flows. The trade-off is operations: you run the app, the GrowthBook Proxy for streaming and caching, and ideally connect a data warehouse for experiment analysis. Featureflip is closed-source hosted SaaS, with supported self-hosting available on the Enterprise plan. So the open-source label clearly favours GrowthBook; the zero-ops hosted experience favours Featureflip. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs LaunchDarkly The other big-platform comparison — flat pricing vs LaunchDarkly's bundled, per-MAU release platform.](/compare/featureflip-vs-launchdarkly/) [### Featureflip vs PostHog The other experimentation-suite comparison — a usage-billed analytics bundle vs a focused, flat-rate flag tool.](/compare/featureflip-vs-posthog/) [### Featureflip vs Unleash Another open-source option — AGPL self-host and per-seat cloud vs hosted flat pricing and SSE streaming.](/compare/featureflip-vs-unleash/) [### Comparing several vendors? The alternatives guides weigh six platforms side by side — GrowthBook, PostHog, Unleash, and the rest.](/alternatives/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All GrowthBook information was sourced from public materials ([pricing](https://www.growthbook.io/pricing) , [feature flags](https://www.growthbook.io/products/feature-flags) , [docs](https://docs.growthbook.io/) , and the [GitHub repository](https://github.com/growthbook/growthbook) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by plan and contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/compare/featureflip-vs-launchdarkly/ # Featureflip vs LaunchDarkly: Pricing and Features (2026) [Home](/) / [Compare](/compare/) / Featureflip vs LaunchDarkly # Featureflip vs LaunchDarkly Pricing, features, and when to pick each — a 2026 comparison. Last updated: September 14, 2026 **Featureflip is the simpler, flat-rate alternative to LaunchDarkly.** LaunchDarkly is the broader release platform — it bundles feature flags with experimentation, observability, AI Configs, and Guarded Releases. Featureflip focuses on feature flags alone, with predictable flat pricing ($0 / $49 / $99 / $249 per month, no per-seat or per-MAU charges). Pick Featureflip if you want a focused flag tool with simple billing. Pick LaunchDarkly if you also need experimentation, observability, or enterprise workflow tooling. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | LaunchDarkly | | --- | --- | --- | | Starting price | $0/mo (Free, no time limit) | $0/mo (Developer, up to 5 service connections and 1K client-side MAU) | | Paid tier (small team) | $99/mo flat ($79 billed annually) — 10 users, unlimited flags | First 5 service connections included, then $12 each + $10/1K client-side MAU ($10 and $8.33 billed annually) | | Per-seat fees | None | None on Developer or Foundation (Enterprise is custom-priced) | | Best for | Teams wanting focused flags + predictable billing | Teams needing flags + experimentation + observability | | Free trial | Free plan, no time limit, no card required | 14-day Foundation trial | ## Pricing comparison The biggest practical difference between Featureflip and LaunchDarkly is the pricing model. Featureflip charges a flat monthly rate. LaunchDarkly charges by usage along multiple axes. ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). No per-seat fees. No per-MAU fees. Unlimited evaluations on every plan. See the [full pricing page](/pricing/) , or read [how flat pricing compares to per-MAU billing](/blog/feature-flag-pricing-explained/) as your user base grows. ### LaunchDarkly pricing As of September 14, 2026, sourced from [launchdarkly.com/pricing](https://launchdarkly.com/pricing/) . We re-verify quarterly. - **Developer** — Free. 5K session replays, 10M logs/traces, unlimited seats and flags. Includes 100K experimentation MAU per month at no additional charge. Bundled with observability since the 2025 Highlight.io acquisition. - **Foundation** — usage-based. The first 5 service connections each month are included. Month-to-month: $12 per additional service connection per month + $10 per 1,000 client-side MAU per month. Annual billing is lower: $10 and $8.33 respectively. - **Enterprise** — Custom. Adds code references, flag scheduling, approval workflows, audit logging, custom roles, and SCIM. - **Guardian** — Paid add-on to Enterprise, priced separately. Adds release monitoring, guardrail metrics, and automatic rollback. ### Estimate your own LaunchDarkly bill Enter your own numbers to compare LaunchDarkly's usage-based Foundation cost against Featureflip's flat price. Defaults to annual billing; switch to month-to-month with the toggle. Server-side service connectionsOne server-side SDK connected to one environment for a month, so the same service in staging and production counts twice. Foundation includes the first 5.Client-side monthly active usersUnique end users your browser and mobile SDKs serve each month.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. Billing (applies to both)Annual (billed yearly)Month-to-month LaunchDarkly Foundation $4,998 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on service connections and client-side MAU. At this usage, LaunchDarkly Foundation runs about **$4,998/year**, roughly **$4,530/year more** than Featureflip Team at a flat $468. LaunchDarkly's bill climbs with your usage. Featureflip's changes only when you move to another plan. LaunchDarkly rates sourced from [launchdarkly.com/pricing](https://launchdarkly.com/pricing/) , checked September 14, 2026. Estimate covers Foundation service connections and client-side MAU only; it excludes AI runs, observability (logs, traces, session replay), experimentation monthly active users, negotiated Enterprise terms and the Guardian add-on. Enterprise is quote-only, and Guardian is an Enterprise add-on priced separately. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [LaunchDarkly cost calculator](/tools/launchdarkly-cost-calculator/) . ### Worked example: small SaaS team A 50,000-MAU B2B SaaS app with 5 backend services and 1 mobile app, with 8 engineers using flags: - **Featureflip Pro:** $99/mo flat — $1,188/year — regardless of MAU. - **LaunchDarkly Foundation** (estimate, month-to-month rates): the first 5 of 6 service connections are included, so 1 × $12 = $12/mo + 50K MAU × $10/1K = $500/mo = **$512/mo** ($6,144/year), before any experimentation use. On annual billing the same profile is about $426.50/mo ($5,118/year). For mid-size SaaS at this profile, Featureflip Pro is roughly **5× cheaper**. The savings ratio shifts as MAU climbs — Featureflip stays flat; LaunchDarkly scales linearly with usage. **Caveat:** LaunchDarkly's value bundle includes experimentation, session replay, logs, and traces. Featureflip does not include these. The right comparison is "feature flags vs feature flags," not "all of LaunchDarkly vs flags." ### Pricing TL;DR - If you want **predictable, budget-able pricing** that doesn't shift with traffic — Featureflip. - If you want **one platform for flags + experimentation + observability** and can absorb usage-based billing — LaunchDarkly. ## Feature comparison Verified against the codebase (Featureflip) and public documentation (LaunchDarkly) as of September 14, 2026. | Feature | Featureflip | LaunchDarkly | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ✅ | | Percentage rollouts | ✅ | ✅ | | Real-time updates (no polling) | ✅ (SSE streaming) | ✅ (streaming) | | Sub-millisecond local evaluation | ✅ | ✅ | | Kill switches | ✅ | ✅ | | Targeting | | | | User targeting | ✅ | ✅ | | Custom attributes | ✅ | ✅ | | Segments / cohorts | ✅ | ✅ | | AND/OR condition groups | ✅ | ✅ | | Prerequisite flags | ✅ | ✅ | | Workflow | | | | Multi-environment | ✅ | ✅ | | Audit log | ✅ (7d / 30d / 90d by plan) | ✅ (Enterprise) | | Stale flag detection | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (flag statuses + flag health) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ✅ (Vega, AI agent) | | Code references (auto-detect flag usage in repo) | ✅ (cleanup Action, 13 languages) | ✅ (Enterprise) | | Scheduled changes | ✅ (paid plans) | ✅ (Enterprise) | | Approval workflows | ❌ | ✅ (Enterprise) | | AI agent flag management (MCP server) | ✅ | ✅ | | Webhooks on flag changes | ✅ (signed, retried and logged, with 1 / 3 / unlimited endpoints by plan) | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP; free plan) | ✅ (.NET, Java, Node.js, PHP, Python, Ruby) | | Experimentation | | | | A/B test variation assignment | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (instrument in your own analytics) | ✅ | | Evaluation event export for third-party experimentation tools | ❌ | ✅ | | Adjacent products | | | | Observability (session replay, logs, traces) | ❌ | ✅ | | AI Configs (runtime AI control) | ❌ | ✅ | | Guarded Releases (auto-rollback on metric breach) | ❌ | ✅ (limited guarded rollouts on every plan, automatic rollback with the Guardian add-on) | | Enterprise | | | | SSO with your identity provider | ✅ (OIDC on Business) | ✅ | | SAML / SCIM | ✅ (Enterprise) | ✅ (SCIM on Enterprise) | | Custom roles / RBAC | ✅ | ✅ (Enterprise) | | SOC 2 | ⚠️ (roadmap) | ✅ | | Self-hosted / on-prem | ✅ (Enterprise) | ❌ (cloud only, including Federal) | | Pricing | | | | Per-seat fees | — (none) | — (none on Foundation) | | Per-MAU fees | — (none) | ✅ ($10/1K) | | Free tier | ✅ (Free, up to 3 people, 10 flags) | ✅ (Developer, up to 5 service connections and 1K client-side MAU) | ## SDK and platform support LaunchDarkly officially supports approximately 30 SDKs across server-side (.NET, Apex, C++, Erlang, Go, Haskell, Java, Lua, Node.js, PHP, Python, Ruby, Rust), client-side and mobile (.NET, Android, C++, Electron, Flutter, iOS, JavaScript, Node.js, React Native, React Web, Roku, Vue), edge (Akamai, Cloudflare, Fastly, Vercel), and AI variants. Featureflip officially supports 13 SDKs covering server-side (JavaScript, Node.js, Python, C#, Java, Go, PHP, Ruby) and client-side (Browser, React, Swift, Flutter, Android). **If your stack uses Erlang, Haskell, Lua, Apex, Roku, Rust, Vue, or you need an edge runtime SDK (Cloudflare Workers, Vercel Edge, Fastly Compute), LaunchDarkly is the better fit today.** Featureflip's REST and streaming APIs work from any HTTP-capable runtime, but you'll write the integration yourself. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick LaunchDarkly when… - **You need experiment analysis in the same tool as flags.** Featureflip handles A/B variation assignment with deterministic-hash rollouts, but doesn't include a dashboard for significance, lift, or metric pipelines. LaunchDarkly covers analysis end-to-end. Featureflip's expected pattern today is app-side instrumentation: when you call `ff.variation()`, fire a parallel event into your own analytics tagged with the variation. Works well with existing analytics; not a clean handoff to GrowthBook yet. - **You need bundled observability.** LaunchDarkly's Highlight.io integration ships session replay, logs, and traces in the same product. Featureflip has no observability product. - **You're building AI features needing runtime prompt or model control.** LaunchDarkly's AI Configs is purpose-built. Featureflip can model AI config as JSON flags but has no dedicated surface. - **You operate in a regulated or federal environment.** LaunchDarkly is SOC 2 Type II with a FedRAMP-authorised Federal tier. Featureflip's compliance roadmap is in-flight, not certified. - **Your stack uses Erlang, Haskell, Lua, Apex, Roku, Rust, Vue, or edge runtimes.** Wider SDK coverage matters here. - **You need approval workflows.** Change approvals are still on the Featureflip roadmap. ### Pick Featureflip when… - **Predictable, flat pricing.** $0 / $49 / $99 / $249 per month. No MAU billing, no per-seat. You can budget without a usage forecast. - **A free tier built for small teams, not for an observability trial.** 10 flags, 2 environments, up to 3 team members, no card. The Free plan is genuinely useful for side projects. - **Focused tool surface.** If you want feature flags — not flags + experimentation + observability + AI Configs — Featureflip's smaller surface is the feature, not a gap. - **Faster to learn.** No service-connection accounting, no MAU dashboards, no observability concepts mixed in. The dashboard does one thing. - **Same evaluation latency profile.** Open SDKs, real-time SSE streaming, sub-millisecond in-process evaluation. Both platforms evaluate locally on the server, so with either one a vendor outage does not stop your app serving flags. SDKs diverge on what happens next, and we have written up [the reconnect behaviour worth checking before you trust one in production](/blog/feature-flag-outage-recovery/) . - **Self-hostable on Enterprise.** For teams that want their own database without paying enterprise-tier prices. - **A flag's whole life runs from your editor.** The [MCP server](/docs/integrations/mcp/) lets Claude Code, Cursor, and other agents create, ramp, and target flags without opening a dashboard. Both platforms ship one. They diverge at removal, where the [cleanup Action](/docs/integrations/flag-cleanup-action/) rewrites the code with AST transforms and no LLM, so the same repository always produces the same diff, and whatever it declines goes back to your agent. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Migrating from LaunchDarkly to Featureflip Most teams choose at adoption time rather than migrating, but if you're switching: - Featureflip's flag concepts (boolean / string / number / JSON variations, targeting rules with condition groups, [audience-based rollouts](/glossary/targeted-rollout/) , percentage rollouts, segments, prerequisite flags) map cleanly to LaunchDarkly's. Migration is mostly find-replace at the SDK call site plus re-creating segments via dashboard or API. - Code references changes shape here. Featureflip scans the repository through its cleanup Action across 13 languages and turns what it finds into a pull request that removes the flag, where LaunchDarkly surfaces the same scan as a browsable view in the dashboard. If that view is what your team relies on day to day, budget for the change in habit. Featureflip's [stale flag detection](/docs/concepts/stale-flags/) decides which flags are finished from evaluation traffic, and the Action then finds where each one is read. Only a flag key assembled at runtime still needs a code search. - Audit log retention drops from LaunchDarkly's longer Enterprise retention to Featureflip's 90 days (Business). Export logs before migrating if you need historical compliance records. ## Frequently asked questions Is Featureflip cheaper than LaunchDarkly? For mid-size SaaS teams (around 50,000 monthly active users with five or more services), yes — Featureflip Pro is $99 per month flat. LaunchDarkly Foundation at the same profile is roughly $500+ per month on month-to-month billing. The first five service connections each month are included. Each one after that is $12, plus $10 per 1,000 client-side monthly active users. Annual billing lowers those to $10 and $8.33. Both platforms have free tiers, but Featureflip's free tier covers teams of up to three people, while LaunchDarkly's free Developer tier is bundled with their observability product (session replays, logs, traces). Does Featureflip support multivariate flags like LaunchDarkly? Yes. Featureflip supports boolean, string, number, and JSON flag types — the same four types LaunchDarkly groups under "boolean and multivariate." You can serve any number of variations per flag and split traffic across them with deterministic-hash percentage rollouts. Can I migrate from LaunchDarkly to Featureflip without rewriting my code? Mostly yes. Both platforms use the same core concepts (variations, targeting rules, segments, percentage rollouts, prerequisite flags), and Featureflip provides SDKs for the most common server-side languages. You will change the SDK initialisation and flag-key lookups — typically a small refactor across the codebase. LaunchDarkly shows code references as a view in its dashboard, while Featureflip's cleanup Action scans your repository across 13 languages and returns them as a pull request that removes a dead flag. Does Featureflip support A/B testing? Yes — Featureflip supports A/B testing via multivariate flags (boolean, string, number, JSON variations) combined with percentage rollouts. The hashing is deterministic, so each user is consistently bucketed into the same variation across sessions and devices, which is what makes the data analysable. What Featureflip does not include is a built-in experiment analysis dashboard — there is no native significance testing, lift calculation, or metric pipeline. The expected pattern is app-side instrumentation: when you call ff.variation(), fire a parallel event into your own analytics stack (Mixpanel, Amplitude, Snowflake, your warehouse) tagged with the variation, then compute lift and significance there. Event export to a dedicated experimentation tool like GrowthBook is on the roadmap. LaunchDarkly's bundled experimentation product covers analysis end-to-end inside the same UI. Is Featureflip SOC 2 compliant? Not yet — SOC 2 certification is on the roadmap. LaunchDarkly is SOC 2 Type II and offers a FedRAMP-authorised Federal tier today. If certification is required for your purchase, LaunchDarkly is the safer choice in 2026. What SDKs does Featureflip support? Officially: JavaScript, Node.js, Python, C#, Java, Go, PHP, and Ruby on the server side, plus Browser, React, Swift, Flutter, and Android on the client side. LaunchDarkly's coverage is wider — if your stack uses Erlang, Haskell, Lua, Apex, Roku, Rust, Vue, or edge runtimes (Cloudflare Workers, Vercel Edge, Fastly Compute), LaunchDarkly is the current fit. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs Flagsmith If you're cross-shopping LaunchDarkly against Flagsmith too — flat pricing vs API-request metering, hosted vs OSS self-host.](/compare/featureflip-vs-flagsmith/) [### LaunchDarkly alternatives guide A broader look — six replacements compared, with a decision rubric for each reason teams switch.](/alternatives/launchdarkly-alternative/) [### Featureflip vs ConfigCat If LaunchDarkly is over budget — ConfigCat is the closest flat-priced alternative, and Featureflip wins on project ceilings.](/compare/featureflip-vs-configcat/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All LaunchDarkly information was sourced from public documentation ([pricing](https://launchdarkly.com/pricing/) , [platform](https://launchdarkly.com/platform/) , [SDK reference](https://launchdarkly.com/docs/sdk) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/compare/featureflip-vs-posthog/ # Featureflip vs PostHog: Feature Flags vs Analytics (2026) [Home](/) / [Compare](/compare/) / Featureflip vs PostHog # Featureflip vs PostHog A focused flag platform with flat pricing and SSE streaming vs an all-in-one analytics suite that bills flags by the request. A 2026 comparison. Last updated: September 14, 2026 **Featureflip and PostHog both manage feature flags, but they are different shapes of product.** PostHog is an all-in-one product suite — analytics, session replay, feature flags, experiments, surveys, error tracking, data pipelines, and AI observability — billed by usage, with a generous free tier and built-in experiment analysis. Featureflip is a focused feature flag platform with flat monthly pricing, real-time SSE streaming, and a free plan. Pick PostHog if you want one tool for your whole product-data stack, or you need statistical experiment analysis built in. Pick Featureflip if you already have analytics and want a focused flag tool with predictable flat pricing instead of per-request billing. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | PostHog | | --- | --- | --- | | Starting price | $0/mo Free (cloud, free forever) | $0 — first 1M flag requests/mo free | | Paid model | $39/mo annual Team — flat, 5 users, unlimited evaluations | Usage-based — from $0.0001/request over 1M (volume discounts) | | Product scope | Focused feature flag platform | All-in-one — analytics, session replay, flags, experiments, surveys, error tracking, data pipelines, and AI observability | | Built-in experiment analysis | No (delivery layer — bring your own analytics) | Yes (statistical significance + lift) | | Real-time updates | SSE streaming, all plans | Polling — client fetch + ~30s server local-eval poll | ## Pricing comparison The biggest practical difference is the pricing model: Featureflip charges a flat monthly fee with unlimited evaluations, while PostHog bills feature flags per request after a generous free tier. Which one is cheaper depends almost entirely on whether your flags run server-side or client-side, and at what volume. ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). Adds self-hosted deployment, SAML/SCIM SSO, custom data retention, dedicated SLA. No per-seat fees. No per-request fees. Unlimited evaluations on every plan. Google and GitHub sign-in are included on every plan, including Free. Business adds single sign-on through your own identity provider over OIDC, and Enterprise adds SAML/SCIM. See the [full pricing page](/pricing/) , or read why [flat pricing beats per-seat and usage-based billing](/blog/feature-flag-pricing-explained/) as your traffic grows. ### PostHog pricing As of September 14, 2026, sourced from [posthog.com/pricing](https://posthog.com/pricing) . We re-verify quarterly. - **Free** — $0, no credit card. Includes the first **1 million feature flag requests/month**, plus free tiers on the other products (events, session recordings, surveys). Experiments are billed together with feature flags. - **Pay-as-you-go** — same 1M/month free, then usage-based per flag request: **$0.0001** (1–2M), **$0.000045** (2–10M), **$0.000025** (10–50M), **$0.00001** (50M+). Every other product is metered on its own usage axis. You can set per-product billing limits. - **Platform add-ons / Enterprise** — SSO/SAML enforcement, advanced permissions, and audit controls are paid platform add-ons. PostHog is SOC 2 Type II compliant. ### Estimate your own PostHog flag bill Enter your monthly feature flag requests and team size to estimate PostHog's per-request cost against the cheapest Featureflip plan that fits. Flags only: PostHog's analytics, session replay, and surveys are metered separately. Monthly feature flag requestsClient-side flags request on page load or session start, so this tracks your monthly active users. Server-side SDKs that evaluate locally are not billed per check, but each poll for flag definitions counts as 10 requests: about 864,000 a month for one server polling every 30 seconds. Add that for each always-on instance.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. PostHog Pay-as-you-go $5,520 per year, estimated Featureflip Pro $948 per year, flat. No usage-based fees on feature flag requests. At this usage, PostHog Pay-as-you-go runs about **$5,520/year**, roughly **$4,572/year more** than Featureflip Pro at a flat $948. PostHog's bill climbs with your usage. Featureflip's changes only when you move to another plan. PostHog rates sourced from [posthog.com/feature-flags/pricing](https://posthog.com/feature-flags/pricing) , checked September 14, 2026. Estimate covers Pay-as-you-go feature flag requests only; it excludes product analytics, session replay, surveys, and data warehouse (each metered on its own usage axis), paid platform add-ons (SSO/SAML enforcement, advanced permissions, audit controls), server-side local evaluation polling unless you add it to the input (each fetch of flag definitions counts as 10 requests, about 864,000 a month for one server polling every 30 seconds). Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [PostHog feature flags cost calculator](/tools/posthog-feature-flags-cost-calculator/) . ### Worked example: server-side flags, under 1M requests/month - **PostHog:** $0 for one always-on server instance. Local evaluation doesn't bill per flag check, but each definition fetch counts as 10 flag requests, so at the default 30-second poll that instance uses about 864,000 of the 1M free requests a month. A second instance takes you to roughly 1.73M, about $72.80/month. - **Featureflip:** Free is free forever, or Team at $39/mo annual for up to 5 seats. At this profile, price is essentially a wash. The decision comes down to product scope (focused flags vs an analytics suite) and propagation model (SSE vs polling), not cost. ### Worked example: client-side flags at scale, 10M requests/month - **PostHog:** 1M free + 1M × $0.0001 ($100) + 8M × $0.000045 ($360) = **~$460/month** ($5,520/year) for feature flags alone. Analytics and session replay, if you use them, are billed on top. - **Featureflip Pro:** $79/mo annual = **$948/year**, flat, unlimited evaluations, no request metering. For client-heavy flag usage at scale, Featureflip is almost **six times cheaper** on flags, a $4,572 a year difference, and the flat fee doesn’t climb as traffic grows. ### Worked example: greenfield, no analytics yet - **PostHog:** flags ride along with product analytics, session replay, experiments, and surveys — each with its own free tier. For a new product under those limits, you get the whole stack for $0. - **Featureflip:** flags only ($0 Free / $39 Team). You add analytics separately (free GA4, or Amplitude/Mixpanel). If you want one tool for the entire product-data stack and don’t already pay for analytics, PostHog’s bundle is the better deal. If you already run analytics, you’d be paying twice for overlapping capability. **Honest caveat:** PostHog’s per-request billing primarily bites *client-side* and high-MAU usage, where every session triggers a flag request. Server-side local evaluation (Node, Python, Go, .NET, and the rest) evaluates from cached definitions and does not bill per check. The periodic definition fetch does bill, at 10 flag requests each, which comes to about 864,000 requests a month for one always-on instance at the default 30-second poll (the Go SDK polls every 5 minutes). So a server-only bill tracks how many instances you run and how often they poll: one instance fits inside the free tier, two run about $72.80/month, and ten about $398.80/month. The real cost variable on PostHog is request volume; on Featureflip it’s flat. And if you already use PostHog for analytics, adding its flags is marginal — the honest question is whether you want a second analytics platform at all. ### Pricing TL;DR - Want **one tool for analytics + flags + experiments** and you’re inside the free tiers — PostHog is hard to beat on bundled value. - Run **client-side flags at scale** — Featureflip’s flat fee is dramatically cheaper than per-request billing, and it doesn’t grow with traffic. - Already have analytics — paying PostHog for flags means a second analytics platform you may not need; a focused flat-priced tool is the cleaner spend. ## Feature comparison Verified against the Featureflip codebase and PostHog’s public documentation as of September 14, 2026. PostHog is the broader product; the rows that matter are scope, pricing model, and propagation, not raw feature count. | Feature | Featureflip | PostHog | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ✅ | | Percentage rollouts | ✅ | ✅ | | Sub-millisecond local evaluation | ✅ | ✅ (server SDKs) | | Real-time updates | ✅ (SSE streaming, all plans) | ⚠️ (polling / reload) | | Kill switches | ✅ | ✅ | | Flag prerequisites / dependencies | ✅ | ✅ | | Remote config (JSON payloads) | ✅ (JSON variations) | ✅ | | Targeting | | | | User targeting | ✅ | ✅ | | Custom attributes / context fields | ✅ | ✅ (person / group properties) | | Segments / cohorts | ✅ | ✅ (cohorts) | | AND/OR condition groups | ✅ | ✅ (release condition sets) | | Workflow | | | | Multi-environment | ✅ (2 / 3 / 5 / unlimited by plan) | ✅ (projects / environments) | | Audit log | ✅ (7d / 30d / 90d by plan) | ✅ (activity log; advanced via add-on) | | Scheduled changes | ✅ (paid plans) | ✅ | | AI agent flag management (MCP server) | ✅ | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP) | ✅ (Python, JavaScript) | | Stale-flag detection / lifecycle | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (usage stats, VS Code extension, Self-driving reports in beta) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ✅ (beta Self-driving in PostHog Desktop, while the VS Code extension refactors JS/TS, Go, and Ruby locally) | | Experimentation | | | | A/B / multivariate variation assignment | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (use external analytics) | ✅ | | Exposure / experiment events | ✅ (events API) | ✅ (native) | | Product scope | | | | Product analytics | ❌ (by design — bring your own) | ✅ | | Session replay | ❌ | ✅ | | Surveys | ❌ | ✅ | | Data warehouse / SQL | ❌ | ✅ | | Error tracking | ❌ | ✅ | | Data pipelines | ❌ | ✅ | | AI observability / PostHog AI | ❌ | ✅ | | Open source & hosting | | | | Open-source license | ❌ | ✅ (MIT outside the commercial ee/ directory, Docker Compose) | | Supported self-hosting | ✅ (Enterprise) | ⚠️ (hobby only; not recommended for prod) | | Hosted SaaS | ✅ | ✅ (Cloud — recommended) | | Enterprise | | | | SSO with your identity provider / SAML | ✅ (OIDC on Business, SAML/SCIM on Enterprise) | ✅ (platform add-on) | | Custom roles / RBAC | ✅ | ✅ (access controls; advanced via add-on) | | SOC 2 Type II | ❌ (not yet) | ✅ | | Pricing | | | | Pricing model | Flat (unlimited evaluations) | Usage-based (per flag request) | | Free hosted / cloud tier | ✅ (Free, up to 3 people, 10 flags) | ✅ (1M flag requests/mo) | | Per-request billing | — (none) | ✅ (over 1M/mo) | | Predictable flat cost | ✅ | ❌ (scales with usage) | ## SDK and platform support Both products cover the mainstream server and client surface. Featureflip ships JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, Flutter, and a dedicated framework-agnostic Browser SDK. PostHog has the broader official catalogue — alongside the mainstream languages it adds Elixir, Rust, React Native, React Router, and Remix, and exposes flags through its analytics SDKs. Both evaluate flags locally in backend SDKs, so the server-side runtime model is similar. **If your stack uses Elixir or Rust today, PostHog has the wider coverage.** For the mainstream server, React, and mobile surface — and for SSE streaming on every plan rather than polling — Featureflip is the better fit. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick PostHog when… - **You want one tool for the whole product-data stack.** Analytics, session replay, flags, experiments, and surveys under one login and one bill — strongest for greenfield products with no analytics yet. - **You need built-in experiment analysis.** PostHog computes statistical significance and lift natively. Featureflip assigns variations and emits exposure events, but leaves the stats to your analytics. - **Your flag usage is modest, or server-side on a small fleet.** Server-side local evaluation doesn't bill per check, and the first 1M feature flag requests a month are free, so one always-on server instance (about 864,000 requests a month in definition polling) or light client traffic effectively rides along free with the rest of PostHog. - **You want MIT-licensed source and the broadest SDK catalogue** (Elixir, Rust, React Native, React Router, Remix). The MIT license covers everything outside PostHog's ee/ directory, which is commercially licensed. - **You’re product-led and already standardizing on PostHog** for analytics — adding its flags is a marginal step, not a new vendor. ### Pick Featureflip when… - **You already have analytics.** If you run Amplitude, Mixpanel, Segment, or GA4, you don’t need a second analytics platform just to flip flags — a focused tool is the cleaner fit. - **You want flat, predictable pricing.** No per-request metering that climbs with traffic and MAU. Client-side flags at scale are dramatically cheaper flat than per request. - **You want real-time SSE streaming on every plan.** PostHog client SDKs fetch on load and server SDKs poll; Featureflip streams flag changes in near real time. - **You want flag removal that runs in CI across your whole stack.** The [cleanup Action](/docs/integrations/flag-cleanup-action/) opens one pull request per finished flag in 13 languages, Swift, Kotlin and Dart included, so mobile code gets cleaned in the same run as the backend. It uses AST transforms and no LLM, which means the same repository always produces the same diff. - **You want a focused flag tool** with a cleaner mental model and no analytics platform to learn, govern, or pay for. - **You’d rather not pipe product analytics and session recordings through a third party** just to manage flags. Featureflip handles flags and nothing else. - **You want supported self-hosting.** It’s available on Featureflip Enterprise; PostHog’s self-host is community-only and discouraged for production. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Can you use PostHog and Featureflip together? Yes, and many teams do. They overlap only on flags; everywhere else they’re complementary. Keep PostHog for product analytics, session replay, and experiment analysis. Use Featureflip as the flag-delivery layer — flat pricing, SSE streaming, and sub-millisecond local evaluation. Featureflip’s events API records exposure events you can forward to PostHog (or any warehouse) for experiment readouts, so you get PostHog’s statistical analysis without paying its per-request flag billing on high-traffic client flags. You take the part of each tool that’s strongest. ### Moving flags off PostHog: concept mapping If you’re consolidating flag management onto Featureflip, the core model translates cleanly. | PostHog | Featureflip | Notes | | --- | --- | --- | | Feature flag (boolean / multivariate) | Feature flag with boolean or multivariate variations | Boolean, string, number, or JSON variations on both sides | | Release condition / property filter | Targeting rule / condition group | Re-express release condition sets as rules with AND/OR groups | | Cohort | Segment | Static cohorts map directly; behavioural cohorts need explicit context fields | | Rollout percentage | Percentage rollout | Deterministic-hash bucketing with sticky assignment on both sides | | Experiment | Variation assignment + exposure events | Analysis stays in PostHog or your analytics; Featureflip delivers the split | | Remote config payload | JSON variation | Serve structured config as a JSON variation value | ### Things to plan for - **Polling / reload to SSE streaming.** Featureflip SDKs receive updates over Server-Sent Events rather than polling or reloading. It’s a net simplification; expect a small change at SDK init, not a rewrite. - **Experiment analysis stays where your data lives.** Featureflip assigns variations and emits exposure events; it doesn’t compute significance. Keep PostHog (or your analytics) for the statistical readout. - **Identity model.** PostHog ties flags to its person and group model. Featureflip uses an explicit user context you pass to the SDK at evaluation time — usually a cleaner, more portable boundary, but it’s an intentional change. - **Scripted flag changes port over.** If you script flag changes against PostHog’s API, Featureflip’s [Management API](/product/features/api/) covers the same ground: flag CRUD, per-environment toggles, targeting rules, and segments over REST, with idempotent creates and every change recorded in the audit log. ## Frequently asked questions Is PostHog’s feature flag product free? Yes for the first 1 million feature flag requests per month. The Free plan needs no credit card, and the same free volume is included on Pay-as-you-go. Beyond it, PostHog bills per request on a sliding scale: $0.0001 from 1–2M, $0.000045 from 2–10M, $0.000025 from 10–50M, and $0.00001 above 50M. Experiments (A/B testing) are billed together with feature flags, not separately. Server-side local evaluation evaluates from cached flag definitions and does not bill per flag check, but each periodic fetch of those definitions counts as 10 flag requests. At the default 30-second poll, one always-on server instance uses about 864,000 requests a month, 86% of the free allowance, so server-side cost grows with instance count and poll interval, whatever your traffic. Client-side flags consume a request per load, which is where request volume adds up. Is Featureflip cheaper than PostHog? It depends on how you use flags. For server-side flags under roughly 1 million requests per month, both are effectively free. For client-side flags at scale, Featureflip’s flat pricing is much cheaper: about 10 million flag requests per month costs roughly $460/month on PostHog for flags alone, versus Featureflip Pro at $79/month annual, flat, with unlimited evaluations. The exception is bundle value — if you have no analytics yet and want product analytics, session replay, experiments, and flags together, PostHog’s free tiers across all of those make it the cheaper way to get the whole stack. Should I use PostHog or Featureflip for feature flags? If you want one tool for your whole product-data stack — analytics, session replay, flags, experiments, surveys, error tracking, data pipelines, and AI observability — or you need built-in experiment statistics, PostHog is the stronger fit, especially greenfield. If you already have analytics (Amplitude, Mixpanel, Segment, GA4) and want a focused flag tool with flat, predictable pricing and real-time SSE streaming, Featureflip is the better fit. The deciding question is usually whether you want to adopt a second analytics platform at all just to manage flags. Does PostHog do A/B testing and experiments that Featureflip doesn’t? Yes — this is a genuine PostHog strength. PostHog ships built-in experiments with statistical significance and lift, computed from its own product analytics. Featureflip handles the delivery side of experiments — sticky variation assignment and exposure events via the events API — and leaves the statistical measurement to your analytics stack. If you want the analysis built in, pick PostHog. If you already measure results in Amplitude, Mixpanel, or your warehouse, Featureflip plugs into that flow without a second analytics bill. Does Featureflip update flags in real time like PostHog? Featureflip pushes flag changes over Server-Sent Events (SSE) on every plan, so SDKs update in near real time without polling. PostHog client SDKs fetch flags on load, and PostHog server SDKs poll flag definitions in the background (every ~30 seconds, or 5 minutes in the Go SDK), so propagation is poll-bound rather than streamed. If near-instant flag changes without a polling interval matter, Featureflip is the better fit. Is PostHog open source and self-hostable? PostHog's main repository is MIT-licensed (its ee/ directory carries a commercial license) and there is a Docker Compose self-host, but PostHog discourages self-hosting for production: it offers no support or guarantees for self-hosted instances, new Kubernetes deployments are no longer supported, and paid features are Cloud-only. PostHog Cloud is the recommended path for any real workload. Featureflip is closed-source SaaS; supported self-hosting is available on the Enterprise plan. So the open-source label favours PostHog, but supported self-hosting actually favours Featureflip. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs LaunchDarkly The other all-in-one platform comparison — a bundled experimentation suite vs a focused, flat-rate flag tool.](/compare/featureflip-vs-launchdarkly/) [### Featureflip vs Unleash Another open-source option — AGPL self-host and per-seat cloud vs hosted flat pricing and SSE streaming.](/compare/featureflip-vs-unleash/) [### Best LaunchDarkly alternatives Also weighing the move off LaunchDarkly? Six platforms compared on pricing, fit, and migration, with a cost-at-scale table.](/alternatives/launchdarkly-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All PostHog information was sourced from public documentation ([pricing](https://posthog.com/pricing) , [feature flags docs](https://posthog.com/docs/feature-flags) , [self-host docs](https://posthog.com/docs/self-host) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by plan and contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/compare/featureflip-vs-unleash/ # Unleash Pricing 2026: $75/Seat vs Featureflip's Flat Rate [Home](/) / [Compare](/compare/) / Featureflip vs Unleash # Featureflip vs Unleash Flat team pricing vs $75-a-seat, a free hosted tier vs open-source-only free — a 2026 comparison. Last updated: September 14, 2026 **Featureflip and Unleash both manage feature flags, but they sell to different buyers.** Unleash is open-source-first and enterprise-grade: a free self-hosted edition plus a per-seat cloud and a contact-sales Enterprise tier, aimed at platform teams that want governance and self-hosting. Featureflip is a closed-source SaaS with flat monthly pricing, a free plan, and SSE streaming on every plan. Pick Unleash if you want to self-host the open-source edition or if you need its breadth and governance enough to pay $75 a seat for them. Pick Featureflip if you want predictable flat hosted pricing without per-seat scaling and real-time streaming without an Enterprise contract. [Try Featureflip free](https://app.featureflip.io/signup) Compare pricing ## At a glance Five-row summary. Detail in the sections below. | | Featureflip | Unleash | | --- | --- | --- | | Starting price | $0/mo Free (cloud, free forever) | $0 self-host OSS only — no free cloud tier | | Paid tier (small team) | $79/mo annual Pro — 10 users, unlimited flags & evaluations | $75/seat/mo Pay-As-You-Go cloud (no flat team tier) | | Open source | No (closed-source SaaS) | Yes (AGPL-3.0 server repo*) | | Self-hostable | Enterprise only | Yes, free (OSS; 1 project / 2 environments) | | Pricing model | Flat — unlimited evaluations | Per-seat ($75/seat/mo) + 53M req/mo cloud cap | ## Pricing comparison The biggest practical differences between Featureflip and Unleash are the cloud pricing model (flat team tiers vs $75 per seat) and the free option (a free hosted plan vs free self-host only). ### Featureflip pricing As of September 14, 2026. - **Free** — $0/mo. 1 project, 10 flags per project, 2 environments, up to 3 team members, 7-day audit log. Free forever. - **Team** — $49/mo ($39/mo annual). 3 projects, up to 5 team members, 3 environments, unlimited flags, scheduled changes, 30-day audit log. - **Pro** — $99/mo ($79/mo annual). 10 projects, up to 10 team members, 5 environments, unlimited flags, scheduled changes, flag owners, 30-day audit log. - **Business** — $249/mo ($199/mo annual). 25 projects, up to 25 team members, unlimited environments, unlimited flags, scheduled changes, flag owners, SSO with your own identity provider (OIDC), 90-day audit log. - **Enterprise** — custom ([sales@featureflip.io](mailto:sales@featureflip.io) ). Adds self-hosted deployment, SAML/SCIM SSO, custom data retention, dedicated SLA. No per-seat fees. No per-API-request fees. Unlimited evaluations on every plan. Google and GitHub sign-in are included on every plan, including Free. Business adds single sign-on through your own identity provider over OIDC, and Enterprise adds SAML/SCIM. See the [full pricing page](/pricing/) , or read why [flat pricing beats per-seat and self-host costs](/blog/feature-flag-pricing-explained/) as your team grows. ### Unleash pricing As of September 14, 2026, sourced from [getunleash.io/pricing](https://www.getunleash.io/pricing) . We re-verify quarterly. - **Open Source** — $0, self-host only. Activation strategies, gradual rollout, variants, constraints, and segments. Limited to **1 project and 2 environments**; SSO, custom RBAC, and change requests require a paid plan. No free hosted tier. - **Pay-As-You-Go** — **$75/seat/month**, cloud or self-hosted (5-seat minimum self-hosted). 14-day free trial, no card. Unlimited flags, projects, and environments; cloud API traffic capped at 53M requests/month with $5 per additional million. Includes SSO (SAML 2.0 or OIDC), role-based access control with custom project roles, and change-request workflows. 99.9% SLA. - **Enterprise** — custom (request a quote). An annual invoiced contract that adds a 99.99% SLA, SSE streaming through Enterprise Edge (Early Access), and optional add-ons such as private instances, multi-region Edge, access logs, and IP restrictions. SSO, SCIM, custom RBAC, change requests, and 2-year audit retention are already included on Pay-As-You-Go, and a dedicated customer success manager comes with Premium support. Note: the "Pro" plan referenced in older Unleash comparisons is no longer offered — the current cloud plan is Pay-As-You-Go. ### Estimate your own Unleash bill Enter your team size and monthly API requests to estimate Unleash cloud's per-seat cost against Featureflip's flat price. Team members (seats)Everyone who needs an Unleash login. This is the main cost driver — Unleash cloud bills per seat from the first one.Monthly API requestsRequests your SDKs make to Unleash cloud each month. Backend SDKs evaluate locally and poll for updates, so this is far lower than your flag-check count. 53 million a month is included; beyond that it is $5 per million. Unleash Pay-As-You-Go $7,200 per year, estimated Featureflip Pro $948 per year, flat. No usage-based fees on cloud seats and API requests. At this usage, Unleash Pay-As-You-Go runs about **$7,200/year**, roughly **$6,252/year more** than Featureflip Pro at a flat $948. Unleash's bill climbs with your usage. Featureflip's changes only when you move to another plan. Unleash rates sourced from [getunleash.io/pricing](https://www.getunleash.io/pricing) , checked September 14, 2026. Estimate covers Pay-As-You-Go cloud seats and API requests only; it excludes the free self-hosted open-source edition (no license fee but you carry the operational cost), the self-hosted 5-seat minimum (it does not apply to the Cloud plan modeled here), quote-only Enterprise contract terms (a 99.99% uptime commitment, streaming through Enterprise Edge, private instances), self-hosted infrastructure and engineering time. Pay-As-You-Go already includes the full Enterprise feature set, SCIM among it. The quote-only Enterprise contract adds a 99.99% uptime commitment, streaming through Enterprise Edge, and add-ons such as private instances. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. Prefer a dedicated tool? Use the full [Unleash cost calculator](/tools/unleash-cost-calculator/) . ### Worked example: tiny team, 1–3 engineers, hosted - **Featureflip Free:** $0/mo, forever, for up to 3 team members. - **Unleash cloud:** no free tier — Pay-As-You-Go for 3 seats = 3 × $75 = $225/mo = **$2,700/year**. For a small hosted team, Featureflip's Free plan is $0 against Unleash's $2,700/year. Unleash cloud has no free hosted tier at all. ### Worked example: small team, 50K-MAU SaaS, 8 engineers - **Featureflip Pro:** $79/mo annual = **$948/year**, 10 seats included, unlimited evaluations. - **Unleash cloud Pay-As-You-Go:** 8 × $75 = $600/mo = **$7,200/year**. At this profile Featureflip Pro is roughly **7.6× cheaper**, because Featureflip prices the team flat and Unleash prices every seat. ### Worked example: growing team, 25 engineers - **Featureflip Business:** $199/mo annual = **$2,388/year**, 25 team members included. - **Unleash cloud Pay-As-You-Go:** 25 × $75 = $1,875/mo = **$22,500/year** at list (or an Enterprise contract — contact sales). Per-seat pricing compounds with headcount. The flat-tier gap widens every time you hire. ### Worked example: free self-host - **Unleash OSS:** $0 in license fees. You operate it (database, upgrades, on-call), accept AGPL-3.0, and live within the 1-project / 2-environment ceiling — governance features (SSO, custom RBAC, change requests) require a paid plan (Pay-As-You-Go or Enterprise, not Enterprise-only). - **Featureflip:** self-hosting is Enterprise only — there is no free self-host tier. If free self-hosting is non-negotiable, Unleash OSS wins on license fee. The fair comparison there is hosting + engineering hours against Featureflip's monthly fee, not list price against $0. **Honest caveat:** Unleash backend SDKs evaluate flags locally and poll for updates, so the 53M-request cloud cap is generous and you are *not* billed per flag check. The real cost variable on Unleash cloud is **seats**, not evaluations — which is exactly where a flat-team plan pulls ahead. ### Pricing TL;DR - Want a **free flag platform you'll operate yourself** and you're comfortable with AGPL-3.0 and the OSS limits — Unleash OSS. - Want **hosted SaaS with flat pricing and a real free tier** — Featureflip is dramatically cheaper than $75/seat at every team size. - Are **scaling headcount** — Featureflip Pro stays flat through 10 seats; Unleash bills every seat, every month. ## Feature comparison Verified against the Featureflip codebase and Unleash's public documentation as of September 14, 2026. | Feature | Featureflip | Unleash | | --- | --- | --- | | Core flags | | | | Boolean flags | ✅ | ✅ | | Multivariate flags (string, number, JSON) | ✅ | ✅ (variants) | | Percentage rollouts | ✅ | ✅ (gradual rollout) | | Sub-millisecond local evaluation | ✅ | ✅ (backend SDKs) | | Real-time updates | ✅ (SSE streaming, all plans) | ⚠️ (polling default; SSE Enterprise) | | Edge / global low-latency runtime | ❌ | ✅ (Unleash Edge) | | Kill switches | ✅ | ✅ | | Flag prerequisites / dependencies | ✅ | ✅ (Pay-As-You-Go+) | | Targeting | | | | User targeting | ✅ | ✅ (constraints) | | Custom attributes / context fields | ✅ | ✅ | | Segments / cohorts | ✅ | ✅ | | AND/OR condition groups | ✅ | ✅ (strategy / constraint composition) | | Workflow | | | | Multi-environment | ✅ (2 / 3 / 5 / unlimited by plan) | ✅ (OSS 2; unlimited Pay-As-You-Go+) | | Audit log | ✅ (7d / 30d / 90d by plan) | ✅ (advanced / 2yr Pay-As-You-Go+) | | Stale-flag detection / lifecycle | ✅ (auto Stale/Dead from evaluation traffic, plus expiry dates, and cleanup notice emails on Team and above) | ✅ (age-based staleness + 5-stage lifecycle) | | Scheduled changes | ✅ (paid plans) | ✅ (Pay-As-You-Go+) | | Approval workflows / change requests | ❌ | ✅ (Pay-As-You-Go+) | | AI agent flag management (MCP server) | ✅ | ✅ | | OpenFeature provider | ✅ (Node.js, .NET, Python, Go, Java, PHP; first-party) | ✅ (first-party Node.js, PHP, Python, Ruby, Rust, Swift beta, plus web, Java, Go, .NET in OpenFeature contrib) | | Removes the dead code by pull request | ✅ (one PR per flag, 13 languages; archives the flag on merge) | ⚠️ (MCP cleanup_flag suggests; you remove) | | Experimentation | | | | A/B / multivariate variation assignment | ✅ | ✅ | | Built-in experiment analysis (significance, lift) | ❌ (use external analytics) | ❌ (use external analytics) | | Open source & hosting | | | | Open-source license | ❌ | ✅ (AGPL-3.0) | | Free self-hosted edition | ❌ | ✅ (1 project / 2 envs) | | Hosted SaaS | ✅ | ✅ (paid only — no free cloud) | | Air-gapped / on-prem | ✅ (Enterprise) | ✅ (Pay-As-You-Go with a 5-seat minimum, or Enterprise) | | Enterprise | | | | SSO with your identity provider | ✅ (OIDC on Business, SAML/SCIM on Enterprise) | ✅ (Pay-As-You-Go+) | | SAML / SCIM | ✅ (Enterprise) | ✅ (Pay-As-You-Go+) | | Custom roles / RBAC | ✅ | ✅ (Pay-As-You-Go+) | | Pricing | | | | Per-seat fees | — (none) | ✅ ($75/seat/mo cloud) | | Free hosted / cloud tier | ✅ (Free, up to 3 people, 10 flags) | ❌ (self-host only) | | Free self-host tier | ❌ | ✅ | | API-request cap | — (unlimited evaluations) | ✅ (53M/mo cloud, $5/M over) | *Unleash's open-source server is licensed AGPL-3.0 per the [GitHub repository](https://github.com/Unleash/unleash/blob/main/LICENSE) (LICENSE / SPDX) as of September 14, 2026. Unleash's documentation now lists AGPL-3.0 from v8.0.0 onward and Apache 2.0 for earlier versions, and its official Docker images remain Apache 2.0. Verify the current license against the repository before making a licensing decision. ## SDK and platform support The two products overlap heavily on the major server and client surface. Both ship JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, and Flutter SDKs. Unleash has the broader official catalogue — it adds Rust, Vue, Svelte, React Native, and Next.js. Featureflip adds a dedicated framework-agnostic Browser SDK. Both evaluate flags locally in backend SDKs, so the runtime model is similar. **If your stack uses Rust, Vue, or Svelte today, Unleash is the better fit on SDK breadth.** For the mainstream server, React, and mobile surface — and for SSE streaming on every plan rather than polling — Featureflip is the better fit. See the [Featureflip SDK overview](/docs/sdks/overview/) for the current supported list. ## Where each tool wins Honest is faster than balanced. Use whichever column matches your situation. ### Pick Unleash when… - **You want to self-host for free and you're comfortable with AGPL-3.0.** The open-source edition has no license fee and a genuine community behind it. The right choice if you have a platform team and prefer running infrastructure to paying for it — mind the 1-project / 2-environment OSS ceiling. - **You need the broadest SDK coverage.** Unleash officially supports Rust, Vue, Svelte, React Native, and Next.js in addition to the mainstream languages. - **You need a global edge runtime.** Unleash Edge ships flag evaluation closer to your users across regions; Featureflip has no edge runtime. - **You're an enterprise that needs formal governance.** SOC 2 Type II, 2-year audit retention, and SCIM provisioning all come with Pay-As-You-Go, and an Enterprise contract adds annual invoicing and a 99.99% uptime commitment. (SSO, RBAC, and change requests are included starting on the $75/seat Pay-As-You-Go plan, not just Enterprise.) - **You value the largest open-source feature-flag community** (~13.8K GitHub stars) and a formal five-stage flag lifecycle. ### Pick Featureflip when… - **You want flat pricing instead of $75 per seat.** Featureflip Team is $39/mo annual for up to 5 seats, and Pro is $79/mo annual for up to 10. Unleash cloud bills every seat, so a team of eight is roughly 7.6× cheaper on Featureflip Pro, and the gap widens with every hire. - **You want a genuinely free hosted tier.** Featureflip's Free plan is free forever in the cloud. Unleash's only free option is self-hosting the limited open-source edition. - **You want real-time SSE streaming on every plan.** Unleash SDKs poll by default and gate true streaming behind Enterprise; Featureflip streams flag changes on Free, Team, Pro, and Business. - **You want flag prerequisites at a flat team price.** Featureflip includes them on every paid plan, starting at $39/mo annual for up to 5 seats on Team. Unleash's flag dependencies start on Pay-As-You-Go at $75 per seat per month. (Unleash's SSO, RBAC, and change-request workflows are on Pay-As-You-Go now too. Featureflip charges one flat monthly price per tier for up to 5, 10, or 25 seats, and Unleash charges for every seat.) - **You want dead flags removed for you.** The [cleanup Action](/docs/integrations/flag-cleanup-action/) runs on a schedule in your CI and opens one pull request per finished flag, across 13 languages. It rewrites the code with AST transforms and no LLM, so a rerun produces the same diff, and it archives the flag once the removal ships. Anything it declines goes to your AI agent over the [MCP server](/docs/integrations/mcp/) . - **You don't want to operate flag infrastructure or pay per seat** just to get governance. Hosted, flat, predictable. - **You want forecastable annual budgeting.** The flat fee is knowable a year out without modelling headcount growth. [Start with the Free plan, free forever](https://app.featureflip.io/signup) ## Migrating from Unleash to Featureflip Most teams choose at adoption time rather than migrating. If you're switching, here's what changes. ### Concept mapping The core data model translates cleanly. Most flag definitions move 1:1. | Unleash | Featureflip | Notes | | --- | --- | --- | | Activation strategy | Targeting rule / rollout strategy | Re-express stacked strategies as rules with condition groups | | Constraint | Condition | Same shape — attribute / operator / value on the user context | | Segment | Segment | 1:1 — recreate via the Featureflip dashboard | | Variant | Variation (multivariate) | Boolean, string, number, or JSON variations on both sides | | Gradual rollout | Percentage rollout | Deterministic-hash bucketing with sticky assignment on both sides | ### Things to plan for - **Activation-strategy stacks → targeting rules.** Unleash composes several activation strategies per environment; Featureflip uses targeting rules with AND/OR condition groups. Most translate directly — re-express multi-strategy stacks as ordered rules. - **Polling → SSE streaming.** Featureflip SDKs receive updates over Server-Sent Events rather than polling. It's a net simplification; expect a small change at SDK init, not a rewrite. - **Change requests.** Unleash's change-request approval flow is a paid-plan feature (Pay-As-You-Go and up) with no Featureflip equivalent yet. Use dashboard RBAC plus audit-log review to gate and review production flag changes, or build your own gate against the [Management API](/product/features/api/) , which every plan includes. - **Self-hosting & AGPL.** If you chose Unleash OSS specifically to self-host or for source availability, Featureflip's hosted model is a different trade-off — self-hosting is Enterprise-only. Weigh your current operational cost against the flat monthly fee. ## Frequently asked questions Is Unleash open source and Featureflip not? Yes. Unleash ships a free, self-hostable open-source edition, and it is the largest open-source feature flag project on GitHub (~13.8K stars). Featureflip is a closed-source SaaS; self-hosting is available only on the Enterprise plan. The nuance is that Unleash’s free OSS edition is limited to 1 project and 2 environments, and it excludes SSO, custom RBAC, and change requests — those are included starting on the $75/seat Pay-As-You-Go cloud plan, not just Enterprise. Scheduled changes and flag dependencies also start on Pay-As-You-Go, while true SSE streaming remains Enterprise-only. If running your own flag infrastructure for $0 in license fees is the requirement and those limits are acceptable, Unleash OSS is the right pick. If you want a hosted product with flat pricing and no infrastructure to operate, Featureflip is built for that trade-off. Is Featureflip cheaper than Unleash? On hosted cloud, yes — substantially. Unleash cloud is Pay-As-You-Go at $75 per seat per month with no free tier (14-day trial only). Featureflip Pro is $79 per month annual ($99 monthly) flat, with 10 seats included. A team of 8 on Unleash cloud costs 8 × $75 = $600 per month ($7,200 a year). The same team on Featureflip Pro is $79 a month ($948 a year), roughly 7.6× cheaper. The only place Unleash is cheaper is self-hosting the open-source edition, where the license fee is $0 and you absorb the operational cost instead. Does Unleash have a free hosted or cloud tier? No. Unleash cloud has a 14-day free trial, then moves to Pay-As-You-Go at $75 per seat per month. The only free option is self-hosting the open-source edition, which you run and operate yourself. Featureflip Free is free forever in the cloud — 1 project, 10 flags, 2 environments — with no card required and no trial clock. Can I self-host Featureflip like Unleash? Self-hosting is available on Featureflip Enterprise. It is not part of the free or Pro tiers. Unleash’s free open-source self-host is the cheaper path if running your own flag platform is non-negotiable — there is no license fee. The trade-offs are operational ownership (database, upgrades, monitoring, on-call), the OSS edition’s 1-project / 2-environment ceiling, and the fact that governance features (SSO, custom RBAC, change requests) sit behind a paid plan — Pay-As-You-Go or Enterprise, not just Enterprise. The open-source server repository is licensed AGPL-3.0 as of mid-2026, which some legal teams treat differently from a permissive license. Does Featureflip support real-time flag updates like Unleash? Yes — and more directly. Featureflip pushes flag changes over Server-Sent Events (SSE) on every plan, so SDKs update in near real time without polling. Unleash SDKs poll the API for changes by default; true SSE streaming is an Enterprise-only feature delivered through Unleash Edge. If real-time propagation without an Enterprise contract matters, Featureflip is the better fit. What SDKs do Featureflip and Unleash support? Heavy overlap on the major server and client surface. Both ship JavaScript, Node.js, Python, .NET, Go, PHP, Ruby, Java, React, iOS, Android, and Flutter. Unleash has broader official coverage — it adds Rust, Vue, Svelte, React Native, and Next.js SDKs. Featureflip adds a dedicated framework-agnostic Browser SDK. Both architectures evaluate flags locally in backend SDKs. If your stack uses Rust, Vue, or Svelte today, Unleash is the better fit on SDK breadth. ## Ready to try Featureflip? Start free on the Free plan — 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started with Featureflip](https://app.featureflip.io/signup) [Talk to sales for Enterprise](mailto:sales@featureflip.io) ## Related [### Featureflip vs Flagsmith The other open-source head-to-head — flat pricing vs API-request metering, hosted SaaS vs OSS self-host.](/compare/featureflip-vs-flagsmith/) [### Featureflip vs LaunchDarkly The other enterprise-grade comparison — flat pricing vs LaunchDarkly's bundled, per-MAU platform.](/compare/featureflip-vs-launchdarkly/) [### Switching from an OSS tool? A broader six-vendor alternatives guide that includes Unleash, Flagsmith, ConfigCat, GrowthBook, and PostHog.](/alternatives/flagsmith-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. The plan details and FAQ behind the comparison.](/pricing/) **Methodology:** All Unleash information was sourced from public documentation ([pricing](https://www.getunleash.io/pricing) , [docs](https://docs.getunleash.io/) , [GitHub repository](https://github.com/Unleash/unleash) ) as of September 14, 2026. We re-verify pricing and feature claims quarterly. Featureflip is our product. Comparison reflects publicly stated capabilities; specific entitlements may vary by edition and contract. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/alternatives/ # Feature Flag Alternatives: Compare the Top Tools (2026) [Home](/) / Alternatives # Feature Flag Alternatives Considering a switch? Honest guides for each major vendor — multi-vendor recommendations, decision rubrics, and migration notes. [## LaunchDarkly Alternative Why teams leave Pricing got expensive as MAU grew, the platform scope (flags + experimentation + observability) is broader than the team needs, or they want simpler flat-rate billing. Read the guide →](/alternatives/launchdarkly-alternative/) [## Flagsmith Alternative Why teams leave The operational cost of self-hosting outgrew the licence-fee savings, hosted SaaS pricing jumped at the 4-seat or 1M-API-request threshold, or they wanted a focused hosted product without committing to OSS. Read the guide →](/alternatives/flagsmith-alternative/) [## ConfigCat Alternative Why teams leave They hit the 3-project ceiling on Pro and won't pay $325/mo for Smart, the SaaS pricing felt expensive once they ran the math against flatter alternatives, or they wanted real-time SSE streaming instead of polling-based cache invalidation. Read the guide →](/alternatives/configcat-alternative/) ## Already shortlisted Featureflip? If you've narrowed it down to Featureflip versus one specific vendor, the head-to-head comparisons cover pricing math, feature parity, and a verdict. If you haven't picked a direction yet, the [category shortlist of flag platforms](/feature-flag-tools/) sorts them by billing model first. [Browse head-to-head comparisons →](/compare/) ## Try Featureflip free Start free on the Free plan: 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/alternatives/configcat-alternative/ # ConfigCat Alternatives: 6 Best Options Compared (2026) [Home](/) / [Alternatives](/alternatives/) / ConfigCat # Looking for a ConfigCat Alternative? How to choose the right replacement — a 2026 guide. Last updated: September 14, 2026 Most teams leaving ConfigCat leave for one of three reasons: - 1.They hit the 3-project ceiling on Pro and won't pay $325/mo for Smart. - 2.The SaaS pricing felt expensive once they [ran the math against flatter pricing models](/blog/feature-flag-pricing-explained/) . - 3.They wanted real-time SSE streaming instead of polling-based cache invalidation. The right alternative depends on which reason matches your team. This page covers the strongest ConfigCat alternatives in 2026 — flat-priced hosted, OSS, and bundled — and helps you pick. [See Featureflip pricing](/pricing/) ConfigCat pricing breakdown Compare alternatives ## Why teams switch from ConfigCat Three patterns dominate the public conversation. The right alternative depends on which one is yours. - 1 ### The 3-project ceiling on Pro ConfigCat Pro at $110/mo caps at 3 products and 100 flags. The 4th product (a new app, an internal tool, an acquisition) forces an upgrade to Smart at $325/mo — a 3× price step that often catches small teams off guard. Multi-app SaaS companies and agencies hit this fastest. - 2 ### Pricing math against flatter alternatives ConfigCat's Pro tier is $110/mo for 3 products; competitors offering 10 projects at half the price look obvious once the math is run. Teams that adopted ConfigCat on the free tier and outgrew it often shop alternatives before committing to the Pro upgrade rather than after. - 3 ### Polling architecture vs real-time streaming ConfigCat SDKs default to polling with webhook-triggered cache invalidation. For teams running incident-response kill switches or anything where seconds-to-propagation matters, the polling model becomes a friction point — they evaluate SSE-streaming alternatives like Featureflip or LaunchDarkly. Other reasons that come up: the network-traffic meter (20 GB free / 100 GB Pro / 1 TB Smart) becoming binding for high-MAU client-side apps, wanting native JSON variations rather than JSON-encoded strings, needing experimentation analysis bundled in the same product, or preferring an open-source option for governance or regulatory reasons. ## ConfigCat pricing in 2026: the full tier breakdown Before comparing alternatives, know exactly what staying costs. ConfigCat's 2026 pricing runs five tiers: **Free ($0), Pro ($110/mo), Smart ($325/mo), Enterprise ($900/mo), and Dedicated ($4,500/mo)**. Every tier, including Free, ships unlimited seats, unlimited monthly active users, all SDKs, and SSO/SAML/SCIM. What you pay for is capacity: products, flags, environments, webhooks, audit-log retention, and a network-traffic meter. | Plan | Price | Products | Flags | Environments | Webhooks | Audit log | Traffic/mo | | --- | --- | --- | --- | --- | --- | --- | --- | | Free | $0 | 2 | 10 | 2 | 1 | 7 days | 20 GB | | Pro | $110/mo | 3 | 100 | 3 | 3 | 35 days | 100 GB | | Smart | $325/mo | 10 | Unlimited | Unlimited | Unlimited | 35 days | 1 TB | | Enterprise | $900/mo | Unlimited | Unlimited | Unlimited | Unlimited | 2 years | 4 TB | | Dedicated | $4,500/mo | Unlimited | Unlimited | Unlimited | Unlimited | 2 years | 24 TB | The price steps are where teams get caught. - Pro caps at 3 products and 100 flags, so the 4th product forces Smart at $325/mo: a $215/mo jump (roughly 3×) to unlock capacity most teams need a fraction of. - The network-traffic allowance is the other step to watch. High-MAU client-side apps can outgrow Pro's 100 GB/month. ConfigCat treats that cap as soft and sells add-on blocks at $95 a month per extra 100 GB, which stays cheaper than Smart until you need a third block. Run your own profile against the alternatives below , or plug in your exact numbers on the [ConfigCat cost calculator](/tools/configcat-cost-calculator/) , before committing to an upgrade. At a 4-to-10-project profile: - [Featureflip's Pro tier](/pricing/) is $99/mo with 10 projects and unlimited flags. - Flagsmith's hosted Start-Up is $45/mo with 1M requests and 3 seats. - Self-hosting Flagsmith or Unleash is $0 if you have the ops capacity. ConfigCat's counterweights are real, though: unlimited seats on every tier, SSO/SAML/SCIM on Free, and the widest SDK coverage of any tool on this page. Figures verified against ConfigCat's public pricing page on September 14, 2026. Prices shown are ConfigCat's annual-billing rates, and month-to-month runs about 10% higher. Vendor pricing changes, so confirm on configcat.com before purchase. ## The strongest ConfigCat alternatives in 2026 Six alternatives covering different fit profiles. Cited links go to vendor sites — we're biased toward Featureflip but believe a fair list earns more trust than a one-vendor pitch. | Tool | Best for | Pricing | OSS? | Self-host? | SSO/SAML | | --- | --- | --- | --- | --- | --- | | Featureflip | Cheapest flat-priced hosted, multi-app teams, SSE streaming | $0 / $49 / $99 / $249/mo flat | No | Enterprise | Google and GitHub sign-in: all tiers; OIDC SSO: Business; SAML/SCIM: Enterprise | | LaunchDarkly | Enterprise bundle: flags + experimentation + observability | Foundation: $10/service connection + $8.33/1K client MAU (annual billing); Enterprise: contact | No | No (cloud only) | SSO and SAML on every plan, SCIM on Enterprise | | Flagsmith | OSS-first teams, hosted SaaS option | Free OSS / $0–$45/mo SaaS Start-Up / Scale-Up $300/mo | Yes (BSD-3-Clause) | Yes (free) | SAML: Scale-Up ($250/mo) | | Unleash | Mature OSS, strong RBAC, enterprise self-host | Free OSS / Pay-As-You-Go $75/seat/mo / Enterprise: contact | Yes (AGPL-3.0) | Yes (free) | SAML/OIDC: all paid plans | | GrowthBook | OSS-first teams that want experimentation analysis built in | Free OSS / Pro $40/seat/mo | Yes (MIT core) | Yes (free) | Google and GitHub SSO on every plan, SAML on Enterprise | | PostHog | Bundle: flags + product analytics + session replay | Free (1M flag requests/mo) / usage-based after | Yes (MIT core) | Yes (free) | SAML: Scale/Enterprise | ### Featureflip Featureflip is the cheaper, project-friendlier flat-rate alternative. Team is $49/mo for 3 projects and unlimited flags, matching ConfigCat Pro's project count at less than half the price. Pro steps up to $99/mo for 10 projects, still under a third of ConfigCat's $325/mo Smart tier. Best fit Teams currently on ConfigCat hosted who hit the 3-project ceiling on Pro and won't pay $325/mo for Smart, or who want SSE streaming for incident-response kill switches rather than ConfigCat's polling model. Weakness SAML/SCIM is Enterprise-only (Google and GitHub sign-in are on every tier and OIDC SSO is on Business; ConfigCat ships SAML/SCIM on every tier including free), no Rust / Elixir / Unity / Unreal / edge-runtime SDKs. [Featureflip website →](https://featureflip.io/) · [Featureflip vs Featureflip →](/compare/featureflip-vs-configcat/) ### LaunchDarkly LaunchDarkly is the premium hosted bundle — flags plus experimentation analysis, observability (via Highlight.io), AI Configs, and Guarded Releases. Best fit Large organisations that genuinely use the full bundle and treat budget as a non-issue. Weakness Usage-based pricing scales with MAU on every billable axis, so cost predictability is the opposite of what you got from ConfigCat hosted. [LaunchDarkly website →](https://launchdarkly.com/) · [Featureflip vs LaunchDarkly →](/compare/featureflip-vs-launchdarkly/) ### Flagsmith Flagsmith is the open-source feature flag platform — BSD-3-Clause, free self-host, and a hosted SaaS for teams that don't want to operate it. Best fit Teams leaving ConfigCat who want OSS guarantees or self-host control as a hedge against vendor changes. Weakness Hosted SaaS meters API requests (from $50 per million over plan), and the Start-Up tier caps at 3 seats, so the 4th hire forces Scale-Up at $300/mo. [Flagsmith website →](https://flagsmith.com/) · [Featureflip vs Flagsmith →](/compare/featureflip-vs-flagsmith/) ### Unleash Unleash is the other mature open-source feature flag platform — AGPL-3.0, free self-host, with hosted Pay-As-You-Go and Enterprise SaaS for teams that don't want to operate it. Best fit ConfigCat-leaving teams who want OSS guarantees plus a strict RBAC model and enterprise governance story. Weakness Hosted Pay-As-You-Go runs $75 per seat per month, which adds up fast for small teams, and the developer experience is more weighty than ConfigCat's simplicity-first dashboard. [Unleash website →](https://www.getunleash.io/) ### GrowthBook GrowthBook leads with experimentation — Bayesian and frequentist analysis, metric pipelines — and offers feature flags as the assignment layer. Best fit Data-team-heavy organisations leaving ConfigCat where experiment results are the goal and flags are a means to the experiment. Weakness Per-seat pricing scales with team size ($40 per seat per month on Pro) and the flag UX is secondary to the experimentation product. [GrowthBook website →](https://www.growthbook.io/) ### PostHog PostHog bundles feature flags with product analytics, session replay, and experimentation. Best fit Teams that want one open-source tool for the entire product-data stack and are willing to take on the operational footprint. Weakness Usage-based pricing on hosted creates the same predictability problem ConfigCat's network-traffic meter does — just on different axes (events, replays). [PostHog website →](https://posthog.com/) ## Featureflip in depth We're biased — Featureflip is our product. Here's the honest version of what it is and isn't compared to ConfigCat. Pricing model $0 / $49 / $99 / $249 per month flat. Nothing metered: no seats, MAU, or network traffic. Team matches ConfigCat Pro's 3-project limit at less than half the price ($49/mo vs $110/mo), and Pro steps up to 10 projects at $99/mo, still under a third of ConfigCat's $325/mo Smart tier. Scope Feature flags + A/B variation assignment. No built-in experiment analysis dashboard, no observability, no AI Configs. Expected experimentation pattern today is app-side instrumentation: fire your own analytics events tagged with the variation and compute lift in your existing analytics stack. SDKs 13 official SDKs (8 server-side, 5 client-side) — JavaScript, Node, Python, .NET, Java, Go, PHP, Ruby, Browser, React, Swift, Flutter, Android. **What ConfigCat has that Featureflip doesn't:** Rust, Elixir, Kotlin Multiplatform, Unity, Unreal Engine, Bun, Deno, Cloudflare Workers, C++ — roughly 1.7× the SDK breadth. If your stack uses any of these, Featureflip is not the right pick. Targeting & rollouts Boolean / string / number / JSON variations (native JSON, unlike ConfigCat's JSON-as-string), AND/OR condition groups in targeting rules, segments, percentage rollouts, kill switches. Real-time updates SSE streaming push from the dashboard to SDKs. ConfigCat's default model is polling with webhook-triggered cache invalidation. For incident-response kill switches and anything where propagation seconds matter, SSE is the cleaner architecture. Flag hygiene Featureflip automatically classifies every flag-environment as Active, [Stale, or Dead from real evaluation traffic](/docs/concepts/stale-flags/) , with reasons attached. ConfigCat's zombie flags report is based on when flags were last changed or toggled — traffic-based classification also catches flags that are still touched but serve one variation to everyone. Each flag can carry an [expiry date](/product/features/flag-lifecycle/) on every plan and a named owner from Pro up, and on Team and up, when a flag expires or goes dead, a cleanup email goes to its owner, or to your admins if nobody owns it. AI agents The [MCP server](/docs/integrations/mcp/) lets Claude Code, Cursor, and other AI assistants create, toggle, target, and clean up flags from the editor. ConfigCat ships an official MCP server too; both manage flag configuration, not runtime evaluation. Hosting model Hosted SaaS only on Free, Team, Pro, and Business. Self-hosting is Enterprise. ConfigCat's Dedicated tier ($4,500/mo) is the equivalent option there, and both are pricier than the OSS self-host story Flagsmith and Unleash offer. What's missing today vs ConfigCat The code-references CLI (repo scanning for flag usage), SAML/SCIM on free tiers (Featureflip includes Google and GitHub sign-in on every tier and OIDC SSO on Business, but requires Enterprise for SAML/SCIM, the single biggest concession to ConfigCat for procurement-gated buyers), longer audit-log retention (Featureflip Business: 90 days; ConfigCat Enterprise: 2 years), and OpenFeature provider breadth (Featureflip ships Node.js, .NET, Python, Go, Java and PHP, and ConfigCat covers 14 platforms). Best fit small-to-mid SaaS engineering teams currently on ConfigCat hosted who hit the 3-project ceiling on Pro, won't justify the jump to Smart at $325/mo, and don't need the SDKs (Rust, Elixir, Unity, Unreal, edge runtimes) or SAML/SCIM-on-free that ConfigCat ships. [Try Featureflip free](https://app.featureflip.io/signup) ## Decision rubric Match your reason for leaving to the right replacement. | If your reason for leaving is… | The right alternative is… | | --- | --- | | You hit the 3-project ceiling on Pro and won't pay $325/mo for Smart | Featureflip (Pro: $99/mo, 10 projects, unlimited flags) | | You want flat hosted pricing but cheaper at a similar feature surface | Featureflip (Team: $49/mo for 3 projects, at less than half ConfigCat Pro's $110/mo) | | You want open source as a hedge against vendor changes | Flagsmith (BSD-3-Clause, free hosted tier or self-host) or Unleash (AGPL-3.0, mature self-host) | | You want experiment analysis primary (significance, lift dashboards), or product analytics bundled | GrowthBook (analysis-primary OSS) or PostHog (flags + analytics bundle) | | You need the full enterprise bundle (flags + experimentation analysis + observability + AI configs) and budget isn't the blocker | LaunchDarkly | ## Migration concerns common to all alternatives Start with the concept mapping — ConfigCat's vocabulary doesn't transfer 1:1. | ConfigCat concept | Maps to | | --- | --- | | Product | Project (Featureflip, Flagsmith, LaunchDarkly) — the top-level container and the unit ConfigCat's plan limits count | | Config | No direct equivalent on most platforms — flags usually live directly in the project, so plan to flatten the Product → Config layer during export | | Setting | Feature flag — boolean, string, number, or JSON variations (Featureflip serves JSON natively rather than as an encoded string) | | Percentage options | Percentage rollout / weighted variations — supported by every alternative on this page | | Targeting rules & segments | Targeting rules and segments — re-test comparator behaviour with production user attributes; semantics differ at the edges between vendors | | Prerequisite flag | Prerequisite / dependent flags — ConfigCat's variation-match model migrates 1:1 to Featureflip and LaunchDarkly, except "does not equal" comparators, which invert to an expected-variation check (migration notes) | | Zombie flags report | Staleness detection — ConfigCat keys off last change/toggle; Featureflip classifies from live evaluation traffic; most OSS alternatives need a code-reference scan instead | - **Product → project export.** ConfigCat exposes flag config via its public API. Pull a JSON snapshot per product before cutover; expect to script a translation per target vendor since there's no universal import format. For Featureflip the import side is scriptable too: recreate flags, targeting rules, and segments through the [Management API](/product/features/api/) instead of clicking them in by hand. - **Polling → push parity.** ConfigCat SDKs poll by default (configurable interval). If you migrate to a push-based platform (Featureflip's SSE, LaunchDarkly's streaming) you'll see propagation drop from polling-interval to seconds — verify your application's expected behaviour under faster updates (e.g. cache TTLs aligned to the old polling cadence). - **Network-traffic accounting.** ConfigCat meters network traffic per tier. Migrating to a flat-priced alternative removes the meter, but if you sized your SDK polling cadence to fit the traffic budget, you can be more aggressive on the new platform without cost penalty. - **SDK rewrites for niche languages.** If your stack uses Rust, Elixir, Kotlin Multiplatform, Unity, Unreal, or edge runtimes (Bun, Deno, Cloudflare Workers), only ConfigCat ships official SDKs in this set. Migrating means a community SDK or an HTTP-evaluation wrapper. - **Cutover strategy.** Run both platforms in parallel for 1–2 weeks behind a kill-switch flag, evaluate concordance, then deprecate ConfigCat. ## Frequently asked questions How much does ConfigCat cost in 2026? ConfigCat's 2026 pricing has five tiers, billed annually: Free ($0: 2 products, 10 flags, 20 GB traffic), Pro ($110/mo: 3 products, 100 flags, 100 GB), Smart ($325/mo: 10 products, unlimited flags, 1 TB), Enterprise ($900/mo: unlimited products, 2-year audit log, 4 TB), and Dedicated ($4,500/mo: private cloud, 24 TB). Month-to-month, Pro is $120, Smart $360, and Enterprise $1,000. All tiers include unlimited seats, unlimited MAUs, all SDKs, and SSO/SAML/SCIM. Figures verified against ConfigCat's public pricing page in September 2026. What is the cheapest ConfigCat alternative? For paid hosted tiers, Featureflip ($0 / $49 / $99 / $249 per month flat) undercuts ConfigCat's Pro tier ($110/mo billed annually, $120 month-to-month) at the same feature surface, and ships 10 projects on Pro vs ConfigCat's 3. For free self-hosted, Flagsmith (BSD-3-Clause), Unleash (AGPL-3.0), GrowthBook (MIT core), and PostHog (MIT core) all offer self-hostable open-source editions at $0, though you take on the operational cost of running them yourself. What is the best open-source alternative to ConfigCat? ConfigCat is closed-source SaaS, so most teams looking for an OSS alternative are making a category switch rather than a like-for-like swap. Flagsmith (BSD-3-Clause) is the most direct comparable for hosted-or-self-hosted flag-only platform. Unleash (AGPL-3.0) is the alternative if RBAC and enterprise governance matter more. GrowthBook (MIT core) and PostHog (MIT core) bundle adjacent products (experimentation analysis and product analytics respectively). Can I migrate from ConfigCat without rewriting my application code? You will change SDK initialisation and re-test segment matches with production user attributes, but the core concepts (variations, targeting rules, segments, percentage rollouts) translate to every alternative on this page. Most teams ship a migration in a one- or two-week refactor sprint. ConfigCat's code-references CLI and its zombie flags report are the surfaces without a 1:1 equivalent on most alternatives. Why do teams leave ConfigCat for Featureflip specifically? Two patterns: hitting the 3-project ceiling on ConfigCat Pro and refusing the $325/mo Smart upgrade, or running pricing math at a 4–10 project profile and finding Featureflip's $99/mo Pro is roughly a third the price of ConfigCat's $325/mo Smart tier at the same project count. The third common pattern is wanting SSE streaming for incident-response kill switches rather than ConfigCat's default polling model. Does any alternative match ConfigCat's combination of flat pricing + SAML/SCIM on free + wide SDK coverage? Not exactly. ConfigCat's three differentiators — flat-rate pricing, SAML/SCIM on every tier including free, and approximately 22 SDKs covering Rust, Elixir, Unity, Unreal, and edge runtimes — are uncommonly bundled. Featureflip matches the flat-rate model (and undercuts on price) and includes Google and GitHub sign-in on every tier and OIDC SSO on Business, but charges Enterprise for SAML/SCIM and ships 13 SDKs. Flagsmith and Unleash match on OSS but not on the SDK breadth or SAML/SCIM-on-free. The combination is genuinely ConfigCat's — leaving means picking which axis you are willing to give up. ## Ready to try Featureflip? Start free on the Free plan: 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [Featureflip vs ConfigCat head-to-head](/compare/featureflip-vs-configcat/) ## Related [### ConfigCat pricing vs Featureflip The plan-by-plan ConfigCat pricing comparison: project ceilings, the $110/mo Pro tier, and where flat-rate Featureflip comes out ahead.](/compare/featureflip-vs-configcat/) [### LaunchDarkly Alternative Companion guide for teams leaving LaunchDarkly — different reasons, overlapping shortlist.](/alternatives/launchdarkly-alternative/) [### Flagsmith Alternative Companion guide for teams leaving Flagsmith — OSS-driven switch reasons, overlapping shortlist.](/alternatives/flagsmith-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. Predictable billing without per-request surprises.](/pricing/) **Methodology:** Pricing and capability claims for ConfigCat and the listed alternatives were sourced from each vendor's public documentation as of September 14, 2026. We re-verify quarterly. Featureflip is our product; this page lists Featureflip alongside competitors because a one-vendor recommendation is not credible. Vendor pricing changes regularly — verify on each linked site before purchase. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/alternatives/flagsmith-alternative/ # Flagsmith Alternatives: 6 Best Options Compared (2026) [Home](/) / [Alternatives](/alternatives/) / Flagsmith # Looking for a Flagsmith Alternative? How to choose the right replacement — a 2026 guide. Last updated: September 14, 2026 **Most teams leaving Flagsmith leave for one of three reasons:** the operational cost of self-hosting outgrew the licence-fee savings, hosted SaaS pricing jumped at the 4-seat or 1M-API-request threshold, or they wanted a focused hosted product without committing to an OSS stack. The right alternative depends on which reason matches your team. This page covers the strongest Flagsmith alternatives in 2026 — hosted, OSS, and bundled — and helps you pick. [See Featureflip pricing](/pricing/) Compare alternatives ## Why teams switch from Flagsmith Three patterns dominate the public conversation. The right alternative depends on which one is yours. - 1 ### Self-hosting operational tax Flagsmith's free BSD-3-Clause edition is genuinely free of licence fees, but a production deployment is a Postgres database, application tier, admin UI, edge proxies, monitoring, backups, version upgrades, and an on-call rotation. For a small team, the staff hours easily exceed the cost of a managed plan elsewhere. The break-even point is the moment teams start shopping for alternatives. - 2 ### Tier jumps on hosted SaaS Start-Up at $45 per month includes 3 seats and 1M API requests per month. The 4th seat forces Scale-Up at $300 per month ($250 on annual billing), and overages start at $50 per additional million requests. Teams that scaled past either threshold without modelling it ahead of time look for flat-rate alternatives. Model your own numbers with the [Flagsmith cost calculator](/tools/flagsmith-cost-calculator/) , or read our guide to [how flat and per-seat feature flag pricing compare](/blog/feature-flag-pricing-explained/) . - 3 ### Wanting hosted without the OSS commitment Some teams adopted Flagsmith for the open-source story but never self-host — they run on the SaaS plan. For them, the OSS positioning is irrelevant to day-to-day use. The question becomes: what is the best hosted feature-flag service for our team size? Flagsmith competes against pure-SaaS vendors on that question. Other reasons that come up: wanting bundled product analytics or experimentation analysis, preferring a different SDK ecosystem, or regional data-residency outside the supported regions. ## The strongest Flagsmith alternatives in 2026 Six alternatives covering different fit profiles. Cited links go to vendor sites — we're biased toward Featureflip but believe a fair list earns more trust than a one-vendor pitch. | Tool | Best for | Pricing | OSS? | Self-host? | | --- | --- | --- | --- | --- | | Featureflip | Predictable flat pricing, focused hosted flag tool | $0 / $49 / $99 / $249/mo flat | No | Enterprise | | LaunchDarkly | Enterprise bundle: flags + experimentation + observability | Foundation: $10/service connection + $8.33/1K client-side MAU billed annually ($12 and $10 month-to-month); Enterprise: contact | No | No (cloud only) | | ConfigCat | Flat-rate hosted alternative | $0 / from $110/mo flat | No | Dedicated / Enterprise | | Unleash | Mature OSS, strong RBAC, enterprise self-host | Free OSS / contact for SaaS | Yes (AGPL-3.0) | Yes (free) | | GrowthBook | OSS-first teams that want experimentation analysis built in | Free OSS / from $40/seat/mo | Yes (MIT core) | Yes (free) | | PostHog | Bundle: flags + product analytics + session replay | Free / usage-based | Yes (MIT core) | Yes (free) | ### Featureflip Featureflip is the flat-rate hosted alternative. It bills $0 / $49 / $99 / $249 per month, with nothing metered: no seats, API requests, or evaluations. Best fit: teams currently on Flagsmith hosted who hit the 4-seat tier jump or the 1M-request overage, or self-hosting teams whose ops cost outgrew the licence savings. Weakness: not open source, no Rust SDK, and no Edge API. If any of those is a hard requirement, Flagsmith is still the right pick. [Featureflip website →](https://featureflip.io/) ### LaunchDarkly LaunchDarkly is the premium hosted bundle — flags plus experimentation analysis, observability (via Highlight.io), AI Configs, and Guarded Releases. Best fit: large organisations that genuinely use the full bundle and treat budget as a non-issue. Weakness: usage-based pricing scales with MAU on every billable axis, so cost predictability is the opposite of what you got from Flagsmith hosted. [LaunchDarkly website →](https://launchdarkly.com/) · [Featureflip vs LaunchDarkly →](/compare/featureflip-vs-launchdarkly/) ### ConfigCat ConfigCat is another flat-rate hosted competitor with strong SDK coverage and a generous free tier. Best fit: teams that want predictable monthly pricing and are comfortable on closed-source SaaS. Weakness: no built-in experimentation or observability, and like Featureflip not open source — same focused-tool tradeoff. [ConfigCat website →](https://configcat.com/) ### Unleash Unleash is the other mature open-source feature flag platform: AGPL-3.0, free to self-host, with a hosted Pay-As-You-Go plan and an Enterprise contract for teams that don't want to operate it. Best fit: teams already running self-hosted Flagsmith who want to stay on OSS but prefer Unleash's RBAC model and stricter governance. Weakness: hosted Pay-As-You-Go pricing ($75 per seat) starts higher than Flagsmith's Start-Up tier, and the developer experience is more enterprise-flavored. [Unleash website →](https://www.getunleash.io/) ### GrowthBook GrowthBook leads with experimentation — Bayesian and frequentist analysis, metric pipelines — and offers feature flags as the assignment layer. Best fit: data-team-heavy organisations where experiment results are the goal and flags are a means to the experiment. Weakness: per-seat pricing scales with team size and the flag UX is secondary to the experimentation product. [GrowthBook website →](https://www.growthbook.io/) ### PostHog PostHog bundles feature flags with product analytics, session replay, and experimentation. Best fit: teams that want one open-source tool for the entire product-data stack and are willing to take on the operational footprint. Weakness: usage-based pricing on hosted creates the same predictability problem at scale, just on different axes (events, replays). [PostHog website →](https://posthog.com/) ## Featureflip in depth We're biased — Featureflip is our product. Here's the honest version of what it is and isn't compared to Flagsmith. - **Pricing model:** $0 / $49 / $99 / $249 per month flat. Nothing metered: no seats, API requests, or MAU. Predictable and budget-able. Team at $49 per month includes 5 team members. Flagsmith Start-Up is $45 per month with 3 seats. - **Scope:** Feature flags + A/B variation assignment. No built-in experiment analysis dashboard, no observability, no AI Configs, no event export to third-party experimentation tools today (roadmap). The expected experimentation pattern today is app-side instrumentation — fire your own analytics events tagged with the variation and compute lift in your existing analytics stack. Flagsmith now offers built-in experiment analysis in beta on Enterprise plans with a connected data warehouse, so an Enterprise team already using it would give that up. - **SDKs:** 13 official SDKs (8 server-side, 5 client-side) — JavaScript, Node, Python, .NET, Java, Go, PHP, Ruby, Browser, React, Swift, Flutter, Android. Heavy overlap with Flagsmith. **What Flagsmith has that Featureflip doesn't:** Rust and Elixir SDKs, and a global Edge API that routes each request to the closest of eight data centers. If your stack uses Rust today or you need an edge runtime for client-side flags worldwide, this is the gap to weigh. - **Targeting and rollouts:** Boolean / string / number / JSON variations, AND/OR condition groups in targeting rules, segments, percentage rollouts, kill switches. - **Flag hygiene:** Automatic [stale and dead flag detection](/docs/concepts/stale-flags/) from evaluation traffic, with reasons per environment. Flagsmith marks a flag stale on Scale-Up and above once nobody has changed it in any environment for 30 days, so a finished flag that someone edited last week still reads as active. Each flag can carry an [expiry date](/product/features/flag-lifecycle/) on every plan and a named owner from Pro up, and on Team and up, when a flag expires or goes dead, a cleanup email goes to its owner, or to your admins if nobody owns it. - **AI agent management:** The [MCP server](/docs/integrations/mcp/) lets Claude Code, Cursor, and other AI assistants create, toggle, target, and clean up flags from the editor. Flagsmith ships an MCP server too; both cover agent-driven flag management over their respective APIs. - **OpenFeature support:** Featureflip ships [OpenFeature providers](/docs/integrations/openfeature/) for Node.js, .NET, Python, Go, Java and PHP, so your application code targets the CNCF vendor-neutral API and swapping providers is a configuration change rather than a rewrite. Flagsmith covers more languages, including browser JavaScript, Kotlin, Ruby, Rust and Swift, so check your language before switching. Either way, writing against the standard is what keeps the next migration cheap. - **Hosting model:** Hosted SaaS only on Free, Team, Pro, and Business. Self-hosting is available on Enterprise, but not as a free OSS download. If $0 self-host is a hard requirement, Flagsmith's open-source edition (or Unleash's) remains the right answer; Featureflip is not trying to compete on that axis. - **What's missing today:** Rust SDK, Edge API, approval workflows. - **Best fit:** small-to-mid SaaS engineering teams currently on Flagsmith hosted who hit a seat-tier jump or API-request overage, or self-hosting teams whose ops cost outgrew the licence savings and who don't need the OSS guarantee. [Try Featureflip free](https://app.featureflip.io/signup) ## Decision rubric Match your reason for leaving to the right replacement. | If your reason for leaving is… | The right alternative is… | | --- | --- | | Self-hosting ops cost outgrew the licence savings, and OSS isn't a hard requirement | Featureflip (flat hosted) or ConfigCat (also flat hosted) | | Hosted SaaS pricing jumped at the 4-seat or 1M-request threshold | Featureflip (flat within each tier, up to 25 team members on Business, with unlimited evaluations) | | You want to stay on open source but prefer different governance / RBAC | Unleash (AGPL-3.0, mature enterprise self-host) | | You want experiment analysis primary (significance, lift dashboards), or product analytics bundled | GrowthBook (analysis-primary OSS) or PostHog (flags + analytics bundle) | | You need the full enterprise bundle (flags + experimentation analysis + observability + AI configs) and budget isn't the blocker | LaunchDarkly | ## Migration concerns common to all alternatives - **Self-host to SaaS data export.** Flagsmith's admin UI exposes flag config; export it before cutover. There's no universal import format; expect to script a translation per target vendor. For Featureflip the import side is scriptable too: recreate flags, targeting rules, and segments through the [Management API](/product/features/api/) instead of clicking them in by hand. - **Edge / latency parity.** Flagsmith's Edge API routes each request to the closest of eight data centers. Featureflip and ConfigCat use CDN-fronted SaaS endpoints which are competitive in most regions but not architecturally identical. If you measured edge-API latency in your selection of Flagsmith, re-measure with the alternative before cutover. - **Rust SDK rewrites.** Flagsmith, Unleash, and ConfigCat ship official Rust SDKs among this list. Moving a Rust service to a tool without one means a thin wrapper over its HTTP evaluation API. - **Identity & traits model.** Flagsmith's "identity + traits" object maps to most alternatives' "context" or "user attributes" model — but the property names and segment-rule operators differ. Re-test segment matches with production traits before cutover. - **Cutover strategy.** Run both platforms in parallel for 1–2 weeks behind a kill-switch flag, evaluate concordance, then deprecate Flagsmith. ## Frequently asked questions What is the cheapest Flagsmith alternative? For paid hosted tiers, Featureflip ($0 / $49 / $99 / $249 per month flat) and ConfigCat (free tier, paid from $110 per month flat) both undercut Flagsmith's hosted SaaS at the Scale-Up tier and above. For free self-hosted, Unleash (AGPL-3.0), GrowthBook (MIT core), and PostHog (MIT core) all offer self-hostable open-source editions at $0. Like Flagsmith's OSS edition, each one leaves you the operational cost of running it. What is the best open-source alternative to Flagsmith? Unleash (AGPL-3.0) is the most direct comparable — a mature OSS feature-flag platform with a free self-host and a paid SaaS option. GrowthBook (MIT core) is the leader if you want experimentation-first OSS. PostHog (MIT core) is the choice if you want flags bundled with product analytics. Featureflip and ConfigCat are not open source; they are SaaS-first. Can I migrate from Flagsmith without rewriting my application code? You will change SDK initialisation and the identity / traits to context mapping, but the core concepts (variations, targeting rules, segments, percentage rollouts) translate to every alternative on this page. Most teams ship a migration in a one- or two-week refactor sprint. The Edge API is the surface without a 1:1 equivalent elsewhere, and a Rust service needs an alternative that ships a Rust SDK, such as Unleash or ConfigCat. Is Flagsmith's free self-hosted edition really free? The licence is free. The total cost of ownership is not. A production deployment requires a Postgres database, application hosting, monitoring, backups, version upgrades, and an on-call response when something breaks. Compare honestly: estimated staff hours per month at your loaded engineering rate, versus a hosted plan elsewhere. Below roughly 5 engineers, hosted usually wins; above that, self-host can be the right call. Does any alternative match Flagsmith's combination of OSS + hosted + edge API? Not exactly. Unleash gets closest with OSS plus hosted plus edge proxies, but the edge architecture differs. GrowthBook and PostHog are OSS plus hosted but optimise for experimentation and analytics respectively. Featureflip, ConfigCat, and LaunchDarkly are hosted-only on standard plans. The combination is genuinely Flagsmith's — leaving means picking which axis you are willing to give up. ## Ready to try Featureflip? Start free on the Free plan: 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [Featureflip vs Flagsmith head-to-head](/compare/featureflip-vs-flagsmith/) ## Related [### Featureflip vs Flagsmith Head-to-head comparison if you've narrowed it down to these two — pricing math, feature parity, migration notes.](/compare/featureflip-vs-flagsmith/) [### Featureflip vs LaunchDarkly Featureflip's other major head-to-head — useful if LaunchDarkly is also on your shortlist after leaving Flagsmith.](/compare/featureflip-vs-launchdarkly/) [### LaunchDarkly Alternative Companion guide for teams leaving LaunchDarkly — different reasons, overlapping shortlist.](/alternatives/launchdarkly-alternative/) [### ConfigCat Alternatives Weighing ConfigCat too? The flat-rate alternatives guide, with cheapest-hosted pricing and project-ceiling trade-offs.](/alternatives/configcat-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. Predictable billing without per-request surprises.](/pricing/) **Methodology:** Pricing and capability claims for Flagsmith and the listed alternatives were sourced from each vendor's public documentation as of September 14, 2026. We re-verify quarterly. Featureflip is our product; this page lists Featureflip alongside competitors because a one-vendor recommendation is not credible. Vendor pricing changes regularly — verify on each linked site before purchase. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/alternatives/launchdarkly-alternative/ # LaunchDarkly Alternatives: 6 Best Options Compared (2026) [Home](/) / [Alternatives](/alternatives/) / LaunchDarkly # LaunchDarkly Alternatives: The 6 Best Options in 2026 A 2026 buyer's guide with real pricing, fit, and migration notes for each. By the Featureflip team · Last updated September 14, 2026 The short answer The strongest LaunchDarkly alternatives in 2026 are **Featureflip**, **ConfigCat**, **Flagsmith**, **Unleash**, **GrowthBook**, and **PostHog**. The right pick depends on why you are leaving. If LaunchDarkly's usage-based MAU pricing has become unpredictable, Featureflip ($0 / $49 / $99 / $249 per month, flat) and ConfigCat are the closest direct replacements. If you want open source with a self-hosted option, Flagsmith (BSD-3-Clause) and Unleash (AGPL-3.0) are the most mature. If experiment analysis is the real goal and flags are secondary, GrowthBook or PostHog fit better. All six cover the core flag primitives (percentage rollouts, targeting rules, segments, and kill switches), and most teams complete a migration in a one to two week SDK refactor sprint. **Most teams leaving LaunchDarkly leave for one of three reasons:** pricing got too expensive as MAU grew, the platform's scope (flags + experimentation + observability) is broader than they need, or they want simpler flat-rate billing. The right LaunchDarkly competitor depends on which reason matches your team. This page weighs the strongest alternatives in 2026 (open-source, low-cost, and focused) and helps you pick. [See Featureflip pricing](/pricing/) Compare cost at scale ## Why teams switch from LaunchDarkly Three patterns dominate the public conversation. The right alternative depends on which one is yours. - 1 ### Usage-based pricing surprise [LaunchDarkly's Foundation plan](https://launchdarkly.com/pricing/) is usage-based: $10 per service connection plus $8.33 per 1,000 client-side MAU on annual billing. A SaaS app that grows from 10K to 100K MAU sees its flag bill climb with users, not flag count. For teams that did not model that trajectory, this is the most common breakup reason. Our guide to [how the three feature flag pricing models compare](/blog/feature-flag-pricing-explained/) walks through that usage-based curve against flat and per-seat billing. You can also [estimate your own LaunchDarkly bill](/tools/launchdarkly-cost-calculator/) and compare it to a flat plan. - 2 ### Scope creep LaunchDarkly's 2024-2025 expansion (Highlight.io acquisition for observability, AI Configs, Guarded Releases) is great if you adopt the bundle. If you only want flags, you're paying for a platform whose value sits in surfaces you don't use. - 3 ### Operational complexity Service-connection accounting, MAU dashboards, experimentation MAU, observability quotas. Every axis is another billing dimension to monitor, and teams that want a focused tool drift away. Other reasons that come up: vendor consolidation (one platform for everything), open-source preference, self-hosting requirements, or regional data residency outside LaunchDarkly's regions. ## The 6 strongest LaunchDarkly alternatives and competitors in 2026 Six alternatives covering different fit profiles, ranked by how directly they replace LaunchDarkly. Vendor links go to each tool's site. We're biased toward Featureflip, but a fair list earns more trust than a one-vendor pitch. | Tool | Best for | Pricing | OSS? | Self-host? | | --- | --- | --- | --- | --- | | Featureflip | Predictable flat pricing, focused flag tool | $0 / $49 / $99 / $249/mo flat | No | Enterprise | | ConfigCat | Most direct flat-rate competitor | $0 / from $110/mo flat | No | Dedicated / Enterprise | | Flagsmith | OSS-first teams, full self-host | Free OSS / from $40/mo SaaS | Yes (BSD-3-Clause) | Yes (free) | | Unleash | Enterprise OSS, strong RBAC | Free OSS / $75/seat/mo | Yes (AGPL-3.0) | Yes (free) | | GrowthBook | Experimentation-first teams | Free OSS / $40/seat/mo | Yes (MIT core) | Yes (free) | | PostHog | Bundle: flags + product analytics | Free / usage-based | Yes (MIT core) | Yes (free) | 1 ### Featureflip Featureflip is the flat-rate alternative. It bills $0 / $49 / $99 / $249 per month, with nothing metered: no seats, monthly active users, or evaluation caps. Best fit: small-to-mid SaaS teams that want flag delivery and predictable billing without platform sprawl. Weakness: no built-in experiment analysis dashboard, no observability bundle, and no event export to third-party tools yet. Pros - +Flat pricing with no per-seat or per-MAU fees - +13 SDKs with sub-millisecond local evaluation Cons - −No built-in experiment analysis or observability bundle **Best migration from LaunchDarkly if** you left LaunchDarkly over MAU-based pricing and want flags plus A/B variation assignment, not a whole platform. [Featureflip website →](https://featureflip.io/) 2 ### ConfigCat ConfigCat is the closest direct flat-rate competitor to LaunchDarkly. Best fit: teams that want predictable pricing and a focused flag platform with strong SDK coverage. Weakness: no experimentation product and no observability bundle, the same focused-tool tradeoff as Featureflip. Pros - +Flat monthly pricing with unlimited seats and MAU - +Broad SDK coverage and a CLI code-reference scanner Cons - −No experimentation product or observability bundle **Best migration from LaunchDarkly if** you want the closest like-for-like flat-rate replacement and do not need experimentation. [ConfigCat website →](https://configcat.com/) · [Featureflip vs ConfigCat →](/compare/featureflip-vs-configcat/) 3 ### Flagsmith Flagsmith is the OSS-first alternative: BSD-3-Clause, with full self-host in the free tier and a SaaS option for teams that do not want to operate it. Best fit: teams that want long-term cost certainty through self-hosting and do not mind the operational overhead. Weakness: enterprise SaaS pricing is contact-sales and the experimentation surface is thinner than LaunchDarkly's. Pros - +Full self-host included in the free OSS edition (BSD-3-Clause) - +Active open-source community and GitHub presence Cons - −SaaS scales on seats and requests; experimentation is thin **Best migration from LaunchDarkly if** you want long-term cost certainty through self-hosting and have a team to operate it. [Flagsmith website →](https://flagsmith.com/) · [Featureflip vs Flagsmith →](/compare/featureflip-vs-flagsmith/) 4 ### Unleash Unleash is the enterprise-grade open-source choice: AGPL-3.0, a mature self-host story, plus a hosted SaaS for teams that do not want to run it. Best fit: large engineering organisations with strong RBAC requirements and a platform team. Weakness: SaaS pricing is per-seat and the dashboard is less polished than the SaaS-native vendors. Pros - +Mature self-host story with strong RBAC (AGPL-3.0) - +Credible with platform-engineering teams Cons - −SaaS is per-seat, and AGPL licensing needs legal review for distributed products **Best migration from LaunchDarkly if** you are a larger org with a platform team and strong access-control requirements. [Unleash website →](https://www.getunleash.io/) · [Featureflip vs Unleash →](/compare/featureflip-vs-unleash/) 5 ### GrowthBook GrowthBook leads with experimentation: statistical analysis, metric pipelines, Bayesian and frequentist methods, with feature flags as the assignment layer. Best fit: data-team-heavy organisations where experiment results are the goal and flags are a means. Weakness: per-seat pricing scales with team size and the flag UX is secondary to the experimentation product. Pros - +Experimentation-first with real statistical analysis (MIT core, self-hostable) - +Feature flags included as the assignment layer Cons - −Per-seat pricing scales with team size; flag UX is secondary **Best migration from LaunchDarkly if** experiment analysis is your primary goal and flags are a means to it. [GrowthBook website →](https://www.growthbook.io/) · [Featureflip vs GrowthBook →](/compare/featureflip-vs-growthbook/) 6 ### PostHog PostHog bundles feature flags with product analytics, session replay, and experimentation. Best fit: teams that want one tool for the entire product-data stack. Weakness: usage-based pricing creates the same predictability problem as LaunchDarkly at scale, just on different axes. Pros - +Bundles flags with product analytics, session replay, and experiments - +Generous free tier for low flag-request volume Cons - −Usage-based pricing reintroduces the unpredictability you left LaunchDarkly for **Best migration from LaunchDarkly if** you want one tool for the whole product-data stack, not a dedicated flag platform. [PostHog website →](https://posthog.com/) · [Featureflip vs PostHog →](/compare/featureflip-vs-posthog/) ## What these alternatives cost at scale A worked scenario: roughly 50,000 client-side monthly active users, a single LaunchDarkly service connection, and a 10-person team (relevant for the per-seat vendors, GrowthBook and Unleash). This is the comparison most LaunchDarkly roundups leave out. | Tool | Est. monthly cost | How it is billed | | --- | --- | --- | | LaunchDarkly (Foundation) | ≈ $417 / mo | About $417 for 50K MAU ($8.33 per 1,000, annual). The single service connection this assumes sits inside the 5 Foundation includes each month. Real teams usually run more, so see the compare page's 6-connection worked example ($512/mo month-to-month). Annual billing, and monthly is higher. | | Featureflip (Pro) | $79 / mo | Flat, billed annually ($99 month-to-month). Unlimited MAU. Pro covers up to 10 team members. | | ConfigCat (Pro) | $110 / mo | Flat, billed annually ($120 month-to-month). Unlimited MAU and seats. | | Flagsmith (Scale-Up) | ≈ $500 / mo | Scale-Up at $250 plus 5 seats past the 5 included, at $50 each on annual billing. $0 if you self-host the OSS edition. | | GrowthBook (Pro) | $400 / mo | $40 per seat for 10 seats. $0 if you self-host the OSS edition. | | Unleash (Pay-as-you-go) | $750 / mo | $75 per seat for 10 seats. $0 if you self-host the OSS edition. | | PostHog | Usage-based | Billed per flag request, not per MAU. Free under 1M requests per month. | Open-source options (Flagsmith, GrowthBook, Unleash) cost $0 in software if you self-host, plus the operational cost of running them. Figures verified against each vendor's public pricing page in September 2026. Pricing changes often, so confirm before purchase. ## Featureflip in depth We're biased, Featureflip is our product. Here's the honest version of what it is and isn't. - **Pricing model:** $0 / $49 / $99 / $249 per month flat. Nothing metered: no seats, monthly active users, or evaluation caps. Predictable and budget-able. - **Scope:** Feature flags + A/B variation assignment. No built-in experiment analysis dashboard, no observability, no AI Configs, no event export to third-party experimentation tools today (roadmap). If you came to LaunchDarkly for the full bundle, this is a downgrade. If you came for the flags and stayed for the bill, this is the point. The expected experimentation pattern today is app-side instrumentation: fire your own analytics events tagged with the variation and compute lift in your existing analytics stack. - **SDKs:** 13 official SDKs (8 server-side, 5 client-side) covering the common stack: JavaScript, Node, Python, C#, Java, Go, PHP, Ruby, Browser, React, Swift, Flutter, and Android. Less coverage than LaunchDarkly's roughly 30. - **Targeting and rollouts:** Boolean / string / number / JSON variations, AND/OR condition groups in targeting rules, segments, percentage rollouts, kill switches. - **Flag hygiene:** Automatic [stale and dead flag detection](/docs/concepts/stale-flags/) from evaluation traffic, the closest equivalent to LaunchDarkly's flag statuses and flag health, with no scanner to set up. Each flag can carry an [expiry date](/product/features/flag-lifecycle/) on every plan and a named owner from Pro up, and on Team and up, when a flag expires or goes dead, a cleanup email goes to its owner, or to your admins if nobody owns it. - **AI agent management:** The [MCP server](/docs/integrations/mcp/) lets Claude Code, Cursor, and other AI assistants create, toggle, target, and clean up flags from the editor. LaunchDarkly ships an MCP server too, so treat this as a floor every mature platform clears. Removal is the half that still separates them. Featureflip pairs the agent with a [cleanup Action](/docs/integrations/flag-cleanup-action/) that rewrites the code deterministically, with no LLM, and leaves the shapes it will not touch for the agent to finish. - **OpenFeature support:** Featureflip ships [OpenFeature providers](/docs/integrations/openfeature/) for Node.js, .NET, Python, Go, Java and PHP, so you write against the CNCF vendor-neutral API rather than a proprietary SDK and migrating becomes a configuration change instead of a rewrite. LaunchDarkly maintains official providers for .NET, Java, Node.js, PHP, Python, and Ruby, so if you need OpenFeature in Ruby specifically, that is a genuine point in its favor. The lock-in argument still runs the other way: coding against the standard is what makes leaving any vendor, including this one, cheap. - **Behaviour during a vendor outage:** Server SDKs evaluate from an in-memory copy of the configuration, so your app keeps serving flags when the control plane is unreachable, and every reconnect re-syncs a full snapshot instead of only resubscribing to future changes. Most mature platforms clear this bar. Treat it as a floor to check rather than a reason to switch, and see [how flag SDKs get stuck after the service comes back](/blog/feature-flag-outage-recovery/) for the questions worth asking any vendor on this list. - **What's missing today:** approval workflows, SOC 2 certification (on roadmap). - **Best fit:** small-to-mid SaaS engineering teams that want flag delivery, predictable billing, and no platform sprawl. [Try Featureflip free](https://app.featureflip.io/signup) ## Decision rubric Match your reason for leaving to the right replacement. | If your reason for leaving is… | The right alternative is… | | --- | --- | | Pricing got expensive as MAU grew | Featureflip (flat) or ConfigCat (also flat) | | You don't need built-in experiment analysis, observability, or AI Configs, just flags + A/B variation assignment | Featureflip or ConfigCat | | You want open source / self-host | Flagsmith or Unleash | | You want experiment analysis primary (significance, lift dashboards), flags secondary | GrowthBook or PostHog | ## Free and open-source LaunchDarkly alternatives Flagsmith (BSD-3-Clause), Unleash (AGPL-3.0), GrowthBook (MIT core), and PostHog (MIT core) are all self-hostable at no software cost. The real cost is operational: the server, upgrades, and maintenance your team takes on. Flagsmith's Helm chart and Unleash's Docker Compose are the two most straightforward self-host setups. One license caveat worth checking with your legal team: the AGPL-3.0 license on Unleash's source can require you to publish changes you make to it and serve over a network, though its official Docker images are Apache 2.0. Flagsmith's BSD-3-Clause and the MIT cores of GrowthBook and PostHog carry no such condition, but both of those projects keep an enterprise directory under a commercial license. If you want a hosted free tier instead of running infrastructure, Featureflip's Free plan and ConfigCat's free tier cover small teams without the operational overhead. ## Migration concerns common to all alternatives - **Code references.** Alternatives differ in whether they scan your repository at all, and in what they hand back when they do. ConfigCat ships a CLI scanner. Featureflip scans across 13 languages and returns a pull request that removes the flag. Check which shape you are getting before you plan the migration, because it decides whether cleanup is something you schedule or something you review. Featureflip's [stale flag detection](/docs/concepts/stale-flags/) tells you which flags are finished from evaluation traffic, and the cleanup Action finds where they're read. You'll only need a code search for a key assembled at runtime. - **Audit log retention.** LaunchDarkly's enterprise retention is generous; alternatives vary. Export historical logs before migrating if compliance needs them. - **SDK rewrites.** All alternatives' SDKs work, but call sites differ. Plan for a feature-flag-call refactor sprint. - **Cutover strategy.** Run both platforms in parallel for 1–2 weeks behind a kill-switch flag, evaluate concordance, then deprecate LaunchDarkly. A typical migration sequence - Export your flag keys and targeting rules from LaunchDarkly's API as JSON. - Recreate the flag structure in the new tool. - Run both SDKs in parallel behind a top-level kill-switch flag for one or two release cycles. - Remove the LaunchDarkly SDK once you have confirmed both platforms evaluate the same. ## Frequently asked questions What is the cheapest LaunchDarkly alternative? For paid tiers, ConfigCat and Featureflip both charge flat monthly rates. On annual billing, Featureflip Team undercuts LaunchDarkly's usage-based pricing from roughly 5,000 client-side monthly active users, and ConfigCat Pro from roughly 13,000. For free, Flagsmith, Unleash, GrowthBook, and PostHog all offer self-hostable open-source editions at $0, though you take on the operational cost of running them yourself. Is there a free LaunchDarkly alternative? Yes. For a hosted free tier, Featureflip's Free plan and ConfigCat's free tier both give you feature flags at $0 with no credit card. For a fully free self-hosted option, Flagsmith (BSD-3-Clause), Unleash (AGPL-3.0), GrowthBook (MIT core), and PostHog (MIT core) all ship open-source editions you can run yourself. The tradeoff with self-hosting is operational: you take on the server, upgrades, and maintenance in exchange for $0 software cost. What is the best LaunchDarkly alternative for small teams? For a small team that wants flags without metered seats or MAU, Featureflip (free to start, then Team at $49 per month flat) and ConfigCat (free, then $110 per month flat) are the most direct fits. Both keep pricing flat as your user base grows, which is usually the reason a small team outgrows LaunchDarkly's usage-based model in the first place. If your team is comfortable self-hosting, Flagsmith's free open-source edition is hard to beat on cost. Why is LaunchDarkly so expensive? LaunchDarkly's entry paid plan (Foundation) is usage-based: roughly $10 per service connection per month plus $8.33 per 1,000 client-side monthly active users on annual billing (verify current pricing on launchdarkly.com). Because the MAU component scales with your user base, the bill grows as your product grows even if your flag count stays the same. That trajectory, plus the broader platform bundle of experimentation, observability, and AI Configs, is what pushes teams that only need flags toward flat-rate alternatives. What is the best open-source alternative to LaunchDarkly? Flagsmith (BSD-3-Clause) and Unleash (AGPL-3.0) are the most mature open-source feature flag platforms. GrowthBook (MIT core) is the leader if you want experimentation-first. PostHog (MIT core) is the choice if you want flags bundled with product analytics. Featureflip and ConfigCat are not open-source; they are SaaS-first. Can I migrate from LaunchDarkly without rewriting my application code? You will change SDK initialisation and flag-key calls, but the core concepts (variations, targeting rules, segments, percentage rollouts, prerequisite flags) translate to most alternatives on this page (Featureflip migrates prereqs 1:1; OSS alternatives vary). Most teams ship a migration in a one- or two-week refactor sprint. Code references has an equivalent here, in a different shape. Featureflip scans the repository through its cleanup Action and returns the result as a pull request that removes the flag, where LaunchDarkly presents the same scan as a browsable dashboard view. Is LaunchDarkly's free tier good enough for small teams? The "Developer" tier is free with unlimited seats, up to 5 service connections and 1,000 client-side monthly active users. It also bundles observability (5,000 session replays plus 10 million logs and traces per month) and 100,000 experimentation MAU. For a small team that wants free flags only, Featureflip's Free plan or ConfigCat's free tier are more focused. For an OSS team, Flagsmith's free self-host is unmatched. Does any alternative have all of LaunchDarkly's enterprise features? Not as a single bundle. LaunchDarkly's combination of flags, experimentation, observability, AI Configs, and Guarded Releases is currently unique. If you need all of those in one product, the question is whether the bundle is worth the price, not whether to leave. If you only need a subset, every alternative on this page covers some part of LaunchDarkly's platform. ## Ready to try Featureflip? Start free on the Free plan: 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [Featureflip vs LaunchDarkly head-to-head](/compare/featureflip-vs-launchdarkly/) ## Related [### Featureflip vs LaunchDarkly Head-to-head comparison if you've narrowed it down to these two: pricing math, feature parity, migration notes.](/compare/featureflip-vs-launchdarkly/) [### Featureflip vs ConfigCat The two flat-rate replacements compared directly, if ConfigCat is also on your shortlist after LaunchDarkly.](/compare/featureflip-vs-configcat/) [### Flagsmith Alternative Companion guide for teams leaving Flagsmith: different reasons, overlapping shortlist.](/alternatives/flagsmith-alternative/) [### Featureflip pricing $0 / $49 / $99 / $249 per month, flat. Predictable billing without per-MAU surprises.](/pricing/) **Methodology:** Pricing and capability claims for LaunchDarkly and the listed alternatives were sourced from each vendor's public pricing pages and documentation, last verified September 14, 2026. We re-verify quarterly. Featureflip is our product; this page lists Featureflip alongside competitors because a one-vendor recommendation is not credible. Vendor pricing changes regularly, so verify on each linked site before purchase. Published by Canopy Labs LLC. --- ## Source: https://featureflip.io/feature-flag-tools/ # Feature Flag Tools: 7 Compared by How They Bill (2026) [Home](/) / Feature flag tools # Feature flag tools A feature flag tool stores the on-off state and targeting rules for your features, then serves that configuration to your code through an SDK. Every tool on this page does that. Where they genuinely differ is what they charge you for, and there are three answers: nothing, your headcount, or your traffic. Your invoice is decided there, well before any feature on a comparison grid comes into it. We make one of these, so read the rest accordingly. The figures below come from each vendor's published pricing page on the date shown, and each one has a calculator that shows the working. ## The meter counts nothing A flat plan quotes one number a month. Traffic can triple overnight and the invoice does not move. The trade is a set of ceilings, so the question to ask a flat vendor is which ceiling you hit first. ### Featureflip Flat monthly price. No seat charge, no evaluation charge. Ours. Free costs nothing, with 10 flags and 2 environments. Team is $49 a month, Pro is $99, and Business is $249. Evaluations are unlimited on every tier, including the free one, because nothing counts requests. Fits: Teams that want the bill to be a fixed line item and do not need experimentation statistics in the same tool. Pricing checked September 14, 2026 [Pricing →](/pricing/) ### ConfigCat Flat tiers gated by product count and monthly network traffic, with add-on blocks for extra traffic. Predictable in the same way, with the ceilings drawn in different places. Forever Free covers 2 products and 20GB of traffic a month. Pro is $110 a month on annual billing for 3 products, and SAML with SCIM is available on every tier rather than held back for an enterprise contract. Fits: Small teams who want flat billing and need SSO early, provided the product count stays low. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-configcat/) [Alternatives →](/alternatives/configcat-alternative/) [Calculator →](/tools/configcat-cost-calculator/) ## The meter counts people Per-seat billing scales with headcount, so it is the easiest of the three to forecast a year ahead. It also puts a price on letting more people near the flags, at the point where most teams are trying to get everyone comfortable using them. ### Unleash $75 per seat a month on Cloud. Open-source-first, with a per-seat hosted plan and a quote-only enterprise tier. There is no free hosted tier, only a 14-day trial, and the cloud plan includes an allowance of 53 million API requests before request pricing enters the picture. Fits: Teams committed to the open-source edition, or small groups who would rather pay per person than watch a traffic meter. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-unleash/) [Calculator →](/tools/unleash-cost-calculator/) ### GrowthBook $40 per seat a month on Cloud Pro, after a free Starter tier for up to three users. Experimentation is the center of gravity here, with flags as the delivery layer under it. It runs a warehouse-native statistics engine offering both Bayesian and frequentist methods, and the paid cloud tier is capped at 50 users before it goes to a quote. Self-hosting the open-source edition costs nothing but your own infrastructure. Fits: Teams that want flags and a real statistics engine in one place and are willing to pay per user for it. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-growthbook/) [Calculator →](/tools/growthbook-cost-calculator/) ## The meter counts traffic This is the group that surprises people, because the unit differs at every vendor and none of them is a straightforward user count. LaunchDarkly runs two meters at once, on open service connections and on browser users. Flagsmith counts API requests. PostHog charges for every individual flag request, which makes it the most sensitive of the three to how your front end is written. Budgeting against the wrong unit is how a forecast ends up out by an order of magnitude. ### LaunchDarkly Two meters at once: $12 per service connection past the first five, and $10 per 1,000 client-side monthly active users. The category incumbent, sold as a platform with experimentation and observability attached. The Developer tier is free up to 5 service connections and 1,000 client-side monthly active users, and enterprise tiers are quote-only. Running both meters together is what makes the bill hard to predict before you are on it. Fits: Large organizations that need the surrounding platform, procurement-grade compliance, and can absorb usage-based billing. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-launchdarkly/) [Alternatives →](/alternatives/launchdarkly-alternative/) [Calculator →](/tools/launchdarkly-cost-calculator/) ### Flagsmith Tiered monthly API requests, with seats fixed per tier. Open-source with a hosted plan and a full self-host path. Free covers 50,000 API requests and one team member. Start-Up is $45 a month with 3 seats and a million requests, and Flagsmith publishes no seat add-on, so a fourth seat means moving to Scale-Up. Requests past a tier bill from $50 per additional million, and the rate falls with volume. Fits: Teams who want the open-source option in reserve, with request volume they can bound. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-flagsmith/) [Alternatives →](/alternatives/flagsmith-alternative/) [Calculator →](/tools/flagsmith-cost-calculator/) ### PostHog Per flag request on a graduated curve, after a free first million a month. Flags are one product inside an analytics and session-replay suite, and they carry no seat cost or subscription of their own. The first million requests each month are free. Past that the rate steps down as volume climbs, so the shape of your traffic matters more than its total. Fits: Teams already running PostHog for analytics, who would rather add flags there than operate a second tool. Pricing checked September 14, 2026 [Compared →](/compare/featureflip-vs-posthog/) [Calculator →](/tools/posthog-feature-flags-cost-calculator/) ## How to narrow the list Four questions separate these tools faster than a feature matrix will. ### What does the meter count, and does your product multiply it? A flag checked once per request in a backend service is a different bill from the same flag checked on every render in a browser. Client-side evaluation multiplies request-metered plans, and it is the single most common way a quote turns out to be wrong. ### Where is the ceiling, and what happens at it? Some ceilings are an overage rate you can absorb. Others are a hard stop that forces a tier upgrade, which is how a fourth teammate can cost more than the three before them. ### Do you need experimentation, or delivery? Serving a sticky 50/50 split is a flag job and every tool here does it. Deciding which variation won, with statistics you would defend in a review, is a different product. If you need both in one place, that narrows the list quickly. If your analytics stack already answers it, paying for a second statistics engine is money spent twice. Our [A/B testing use case](/use-cases/a-b-testing/) draws that line in more detail. ### Hosted or self-hosted? Self-hosting an open-source edition removes the license line and adds an operational one. The honest comparison is the license against a database, upgrades and an on-call rotation, because a flag service sits in the path of every feature you ship. The [build versus buy guide](/blog/build-vs-buy-feature-flags/) works through that arithmetic. ## Go deeper on any of them This page is the shortlist. Three clusters sit underneath it, each answering a different question about the same set of tools. [### Comparisons Feature-by-feature against each platform, with a verdict per team profile.](/compare/) [### Alternatives guides For teams already leaving a platform and choosing where to land.](/alternatives/) [### Cost calculators Put your own numbers through a vendor's rate card and read the annual figure.](/tools/) If the pricing models themselves are what you are trying to understand, the [deep dive on feature flag pricing](/blog/feature-flag-pricing-explained/) covers all three meters and the traps in each. ## Frequently asked questions ### What is a feature flag tool? A feature flag tool stores the on-off state and targeting rules for features in your application, and serves that configuration to your code through an SDK. It lets you turn a feature on for some users and off for others without deploying again, and turn it off entirely when something breaks. ### How much do feature flag tools cost? Free tiers are common and paid plans typically start between $40 and $110 a month for a small team. The spread comes from what each vendor meters rather than from the headline price, so two tools quoting a similar starting figure can bill very differently once traffic or headcount grows. ### What is the difference between flat and usage-based feature flag pricing? A flat plan charges one price a month regardless of how many times your code checks a flag. A usage-based plan meters something that grows with your product, such as API requests, flag requests, open service connections, or client-side monthly active users. Flat pricing is predictable and bounded by ceilings. Usage-based pricing starts cheaper and moves with your traffic. ### Which feature flag tools have a free tier? Featureflip, ConfigCat, Flagsmith, PostHog, GrowthBook and LaunchDarkly all publish a free tier, though the caps differ sharply: 1 team member at Flagsmith, 3 users at GrowthBook, 5 service connections and 1,000 client-side monthly active users at LaunchDarkly. Unleash has no free hosted tier and offers a 14-day trial instead. ### Can you self-host a feature flag tool? Flagsmith, Unleash and GrowthBook all publish an open-source edition you can run yourself, and self-hosting removes the license cost entirely. It replaces that cost with your own: a database, an uptime commitment, upgrades, and an on-call rotation for a service that sits in the request path of every feature you ship. ### Do I need a feature flag tool, or should I build one? A boolean in a config file is a reasonable first flag and plenty of teams stop there. The cost of building arrives later, in the parts that are dull to write and unpleasant to get wrong: per-environment configuration, sticky percentage bucketing, an audit trail, and an SDK that keeps serving the last known values when the network is down. ## Try the flat one Featureflip is one price a month whatever your traffic does. The Free plan costs nothing, with 10 flags and 2 environments, and it does not ask for a card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/tools/ # Feature Flag Cost Calculators (2026) [Home](/) / Cost calculators # Feature flag cost calculators Put your own numbers through a vendor's published rate card and see the annual figure next to Featureflip's flat price. [## LaunchDarkly cost calculator Usage-based Foundation pricing, charged on service connections and client-side monthly active users. You enter: Service connections, client-side MAU Run the numbers →](/tools/launchdarkly-cost-calculator/) [## Flagsmith cost calculator Metered SaaS billing on monthly API requests, with seat tiers layered on top. You enter: Monthly API requests, team size Run the numbers →](/tools/flagsmith-cost-calculator/) [## ConfigCat cost calculator Tiered plans priced on product count and monthly network traffic, with add-on blocks when traffic runs over. You enter: Products, monthly network traffic Run the numbers →](/tools/configcat-cost-calculator/) [## Unleash cost calculator Per-seat Pay-As-You-Go cloud billing. API request volume is charged alongside it. You enter: Team size, monthly API requests Run the numbers →](/tools/unleash-cost-calculator/) [## PostHog cost calculator Per-request billing on a graduated rate curve. Feature flags only, not the wider analytics bundle. You enter: Monthly flag requests Run the numbers →](/tools/posthog-feature-flags-cost-calculator/) [## GrowthBook cost calculator Per-seat cloud billing, with a free three-user Starter tier and a fifty-user ceiling on Pro. You enter: Team size Run the numbers →](/tools/growthbook-cost-calculator/) ## Where the rates come from Every rate is taken from the vendor's own published pricing page, and each calculator carries the source link and the date it was last checked. Quote-only enterprise tiers stay quotes rather than turning into a guess. The worked examples behind each calculator are unit-tested, so a rate that drifts out of date fails our build instead of quietly misleading you. These are estimates, not quotes. Discounts and negotiated terms move the real number. ## Want the feature comparison too? Price is one input. The comparison guides cover SDK coverage, targeting, streaming, and where each platform genuinely fits better than Featureflip. For a shorter route, the [rundown of feature flag platforms](/feature-flag-tools/) sorts every vendor here by the unit it meters. [Read the comparisons →](/compare/) [Browse alternatives guides →](/alternatives/) ## Skip the arithmetic Featureflip is one flat price per plan, whatever your traffic does. Start free on the Free plan with 10 flags and 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/tools/configcat-cost-calculator/ # ConfigCat Cost Calculator — Your Bill vs Flat Pricing # ConfigCat cost calculator ConfigCat gates plans by product count and monthly network traffic. Enter your usage to estimate the bill, then see it against Featureflip's flat price. Products (projects)Each ConfigCat "product" — roughly a Featureflip project.Monthly network traffic (GB)Network traffic ConfigCat's SDKs generate downloading your config each month, across all environments. The tier ceilings assume about 4GB per million downloads, but an unchanged config comes back as a small 304 response, so real traffic depends on your config size and how often it changes.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. Billing (applies to both)Annual (billed yearly)Month-to-month ConfigCat Pro $1,320 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on products and monthly network traffic. At this usage, ConfigCat Pro runs about **$1,320/year**, roughly **$852/year more** than Featureflip Team at a flat $468. ConfigCat's bill climbs with your usage. Featureflip's changes only when you move to another plan. ConfigCat rates sourced from [configcat.com/pricing](https://configcat.com/pricing) , checked September 14, 2026. Estimate covers Pro/Smart/Enterprise products and monthly network traffic only; it excludes flags/settings and environment ceilings (fixed per tier, not usage-metered), config JSON download caps (5M / 25M / 250M / 1B a month by tier), which can bind before traffic when configs are small, the annual-only Dedicated plan ($4,500 a month, private cloud or on-premise), which can undercut Enterprise plus add-ons at very high traffic, student/teacher and startup program discounts, negotiated Enterprise/Dedicated terms. Past 1,000 products, ConfigCat raises its technical limits on request, so there is no published price to model. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [configcat.com/pricing](https://configcat.com/pricing) , checked September 14, 2026. - **Forever Free** — $0/mo. 2 products, 10 flags, 20GB/mo traffic. - **Pro** — $110/mo billed annually ($120 month-to-month). 3 products, 100 flags, 100GB/mo traffic, add-on blocks at $95/mo. - **Smart** — $325/mo billed annually ($360 month-to-month). 10 products, unlimited flags, 1TB/mo traffic, add-on blocks at $270/mo. - **Enterprise** — $900/mo billed annually ($1,000 month-to-month). Roughly 1,000 products (negotiable), 4TB/mo traffic, 2-year audit log, add-on blocks at $760/mo. - **Dedicated** — $4,500/mo, annual billing only. Private cloud or on-premise, 24TB/mo traffic. Not modeled here, because it is a deployment choice rather than a usage tier. This estimate covers products and network traffic on ConfigCat's hosted plans. It excludes flags/ environment ceilings (fixed per tier, not usage-metered), config.json download caps, the Dedicated private-cloud plan, and student/teacher/startup program discounts. ## Featureflip is flat, whatever your traffic The Free plan costs nothing for 1 project, Team is $49 per month ($39 a month, $468 a year, billed annually) for 3 projects, Pro is $99 per month ($79 a month, $948 a year, billed annually) for 10 projects, and Business is $249 per month ($199 a month, $2,388 a year, billed annually) for 25 projects, with no per-product overage and no network-traffic meter. See the [full pricing page](/pricing/) , or read the [Featureflip vs ConfigCat comparison](/compare/featureflip-vs-configcat/) for feature parity and SDK coverage notes. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does ConfigCat cost? ConfigCat's hosted plans are flat tiers with product ceilings and network-traffic allowances, and annual billing saves up to 10%. Forever Free covers 2 products and 20GB/mo of traffic. Pro is $110 per month billed annually ($120 month-to-month) for 3 products and 100GB/mo. Smart is $325 per month ($360 month-to-month) for 10 products and 1TB/mo. Enterprise is $900 per month ($1,000 month-to-month) for roughly 1,000 products (a negotiable technical limit) and 4TB/mo. Rates were checked on September 14, 2026 from configcat.com/pricing. What happens if I exceed my plan's product or traffic limit? Nothing is billed automatically — ConfigCat charges no per-unit overage fee. Exceeding a product, flag, or environment cap on Free, Pro, or Smart requires upgrading to the next tier. Traffic and config.json download limits are soft. ConfigCat says it will keep serving your data and flags, and each paid tier sells an add-on block at the same price on monthly or annual billing: $95/mo on Pro adds 25M downloads and 100GB, $270/mo on Smart adds 250M and 1TB, and $760/mo on Enterprise adds 1B and 4TB. Dedicated, the tier above Enterprise, is $4,500/mo on annual billing only, with 24TB/mo of traffic, hosted or on-premise. A few limits on the higher tiers (flags, environments) are marked as "technical limits" in ConfigCat's docs and are raised by contacting support rather than billed. What forces an upgrade from Pro to Smart? Any one of Pro's three caps: a 4th product, a 101st flag, or a 4th environment. Going past Pro's 100GB/mo of traffic, or its config.json download allowance, means buying a $95/mo add-on block instead (25M more downloads and 100GB), and Pro plus one block is $205/mo on annual billing against $325 for Smart. How is this estimate calculated? The calculator checks your products and monthly network traffic against ConfigCat's Forever Free caps first. Past those, it prices every paid tier whose product ceiling fits, adds add-on blocks wherever your traffic runs past that tier's allowance, and shows the cheapest result. That is how Pro plus one $95 block ($205 a month on annual billing) comes out ahead of Smart at $325. Featureflip's side picks the cheapest Featureflip plan that fits your project count and team size, a flat price that does not change with traffic. Every rate is sourced and dated on this page. --- ## Source: https://featureflip.io/tools/flagsmith-cost-calculator/ # Flagsmith Cost Calculator — Your Bill vs Flat Pricing # Flagsmith cost calculator Flagsmith meters by API requests and caps seats per tier. Enter your monthly requests and team size to estimate the bill, then see it against Featureflip's flat price. Monthly API requestsTotal flag/identity evaluation calls Flagsmith counts each month, across all environments.Team membersEveryone with access to your Flagsmith organisation. Billing (applies to both)Annual (billed yearly)Month-to-month Flagsmith Scale-Up $3,000 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on API requests and team members. At this usage, Flagsmith Scale-Up runs about **$3,000/year**, roughly **$2,532/year more** than Featureflip Team at a flat $468. Flagsmith's bill climbs with your usage. Featureflip's changes only when you move to another plan. Flagsmith rates sourced from [flagsmith.com/pricing](https://www.flagsmith.com/pricing) , checked September 14, 2026. Estimate covers Start-Up/Scale-Up API requests and team members only; it excludes self-hosted / on-prem deployment (free license, but you carry the operational cost), negotiated Enterprise terms. Enterprise (more than 20 seats, LDAP or SCIM, or custom hosting) is quote-only. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [flagsmith.com/pricing](https://www.flagsmith.com/pricing) , checked September 14, 2026. - **Free** — $0/mo. 50,000 API requests, 1 team member. - **Start-Up (annual billing)** — $40/mo. 1 million API requests, 3 team members, no published seat add-on. - **Start-Up (month-to-month)** — $45/mo. Same caps. - **Scale-Up (annual billing)** — $250/mo. 5 million-plus API requests, 5 team members included, extra seats up to 20 at $50/seat/month. - **Scale-Up (month-to-month)** — $300/mo. Same caps, with extra seats at $60/seat/month. - **Overage** — $50 per additional million API requests, on every paid tier. The rate falls from there as volume grows. - **Enterprise** — custom, quote-only. Required past 20 seats, or for on-prem/air-gapped hosting and LDAP/SCIM. This estimate covers Flagsmith's hosted SaaS. It excludes the free, self-hosted open-source edition (no license fee, but you carry the operational cost), Edge API usage, and negotiated Enterprise terms. ## Featureflip is flat, whatever your traffic The Free plan costs nothing, Team is $49 per month ($39 a month, $468 a year, billed annually), Pro is $99 per month ($79 a month, $948 a year, billed annually), and Business is $249 per month ($199 a month, $2,388 a year, billed annually), with no per-request or per-seat overage. See the [full pricing page](/pricing/) , or read the [Featureflip vs Flagsmith comparison](/compare/featureflip-vs-flagsmith/) for feature parity and self-hosting notes. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does Flagsmith cost? Flagsmith's hosted SaaS is metered by monthly API requests, with a fixed number of team seats per tier. Free covers 50,000 requests and 1 team member. Start-Up is $40 per month on annual billing ($45 month-to-month) for 1 million requests and 3 team members. Scale-Up is $250 per month annual ($300 month-to-month) for 5 million-plus requests and 5 team members, with extra seats up to 20 at $50 per seat per month on annual billing ($60 month-to-month). Requests beyond a tier's allowance bill from $50 per additional million, and the rate falls with volume. Enterprise (20+ seats, custom hosting, or Okta, ADFS, and LDAP integrations) is quote-only. Rates were checked on September 14, 2026 from flagsmith.com/pricing. Can I avoid Flagsmith fees by self-hosting? Yes — Flagsmith's community edition is open source (BSD-3-Clause) and free to self-host, with no license fee and no seat cap. The trade-off is operational ownership: you run the database, application, and upgrades, and you're on call when something breaks. This calculator only estimates the metered hosted SaaS; self-hosting cost is your infrastructure and engineering time, not a number this tool can price for you. What forces an upgrade from Start-Up to Scale-Up? Two things: exceeding 1 million API requests a month, or needing a 4th team seat. Start-Up includes 3 seats and has no published seat add-on, so on list pricing a 4th hire means Scale-Up even if request volume is still small. Scale-Up includes 5 seats and lets you add more, up to 20, at $50 per seat per month on annual billing or $60 month-to-month. How is this estimate calculated? The calculator checks your API requests and team members against Flagsmith's free-tier caps first, then the lowest paid tier whose included requests and seats cover your usage. If your usage exceeds even Scale-Up's caps, it adds $50 per additional million requests and $50 per additional seat on annual billing ($60 month-to-month), up to 20 seats total. The $50-per-million figure is Flagsmith's starting overage rate, which falls with volume, so estimates at high traffic are an upper bound. Beyond 20 seats, Flagsmith is quote-only Enterprise. Featureflip's side picks the cheapest Featureflip plan that fits your team size, a flat price that does not change with request volume or requests-per-second traffic. Every rate is sourced and dated on this page. --- ## Source: https://featureflip.io/tools/growthbook-cost-calculator/ # GrowthBook Cost Calculator — Per-Seat Bill vs Flat Pricing # GrowthBook cost calculator GrowthBook Cloud is free for three people, then $40 per seat per month. Enter your team size to estimate the bill, then see it against Featureflip's flat price. Team members (seats)Everyone who needs a GrowthBook login, since GrowthBook counts every user of the platform as a seat. End users of your app are not counted. The first 3 are free on Starter. Add a 4th and the whole team moves to Pro, where every seat costs $40 a month. GrowthBook Pro (Cloud) $2,400 per year, estimated Featureflip Team $468 per year, flat. No usage-based fees on Cloud seats. At this usage, GrowthBook Pro (Cloud) runs about **$2,400/year**, roughly **$1,932/year more** than Featureflip Team at a flat $468. GrowthBook's bill climbs with your usage. Featureflip's changes only when you move to another plan. GrowthBook rates sourced from [growthbook.io/pricing](https://www.growthbook.io/pricing) , checked September 14, 2026. Estimate covers Pro (Cloud) Cloud seats only; it excludes the free self-hosted open-source edition (no licence fee but you run the app, the proxy, and your warehouse), CDN overage on Pro ($10 per million requests past 2 million, $1 per GB past 20GB), managed-warehouse events past the first 2 million ($0.03 per 1,000), quote-only Enterprise terms (SAML or OIDC SSO, SCIM, custom roles, approval flows, a 99.99% SLA). Above 50 users, GrowthBook Cloud moves to quote-only Enterprise, so this estimate stops rather than extrapolating the seat rate. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [growthbook.io/pricing](https://www.growthbook.io/pricing) , checked September 14, 2026. - **Starter (Cloud)** — $0, no credit card. Up to **3 users** on 1 project, with unlimited feature flags, experiments, and traffic. - **Pro (Cloud)** — **$40/seat/month**, up to 50 users and 3 projects. Adds advanced statistics (CUPED, sequential testing, multi-arm bandits), the visual A/B editor, sticky bucketing, safe rollouts, and Product Analytics. - **Enterprise (Cloud)** — quote-only. Adds OIDC/SAML SSO, custom roles, data pipelines, approval flows, exportable audit logs, and a 99.99% SLA. - **Open Source (self-hosted)** — $0 in license fees, unlimited users. You run the application, the GrowthBook Proxy, and your warehouse. Seats are what move a GrowthBook bill, so that is what this estimate models. GrowthBook also meters CDN requests, bandwidth, and managed-warehouse events, but those allowances are published per plan and sit apart from the seat rate, so they are excluded here rather than guessed at. The estimate also excludes the free self-hosted open-source edition, where the license costs nothing and you carry the operational work instead. GrowthBook publishes no annual-commit discount, so there is no annual toggle on its side. ## Featureflip is flat, whatever your headcount The Free plan costs nothing for up to 3 team members, Team is $49 per month ($39 a month, $468 a year, billed annually) for up to 5, Pro is $99 per month ($79 a month, $948 a year, billed annually) for up to 10, and Business is $249 per month ($199 a month, $2,388 a year, billed annually) for up to 25. Nothing is metered: no per-seat or per-request fees on any plan. Featureflip serves variations and emits exposure events, and hands measurement to your analytics stack, so it does not replace GrowthBook's statistics engine. See the [full pricing page](/pricing/) , or read the [Featureflip vs GrowthBook comparison](/compare/featureflip-vs-growthbook/) for where each tool fits. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does GrowthBook cost? GrowthBook Cloud is free on the Starter plan for up to 3 users on 1 project, with unlimited flags, experiments, and traffic. Past that, Pro costs $40 per seat per month and covers up to 50 users, adding advanced statistics, the visual A/B editor, sticky bucketing, and Product Analytics. Above 50 users, Cloud is quote-only Enterprise. The self-hosted open-source edition is free for unlimited users. Rates were checked on September 14, 2026 from growthbook.io/pricing. Does GrowthBook have a free tier? Yes, two of them. Starter is a free hosted plan for up to 3 users on 1 project, so a very small team can run GrowthBook Cloud without paying anything. The open-source edition is free for unlimited users if you self-host it, which means running the application, the GrowthBook Proxy, and ideally a data warehouse. Featureflip's Free plan is free forever in the cloud with no card, which is what a zero-seat estimate compares against here. Why does my GrowthBook bill grow when my traffic does not? Because Pro is priced mainly by headcount. Every person with a GrowthBook login costs another $40 a month, so onboarding four teammates adds $1,920 a year even when your flag count and evaluation volume are unchanged. End users of your app are never counted, and flags and experiments stay unlimited on every Cloud plan, so seats are the main driver of the bill. Pro also meters usage past its allowances: CDN overage is $10 per million requests beyond 2M and $1 per GB beyond 20 GB, and the managed warehouse charges $0.03 per 1,000 events after the first 2M free. Is GrowthBook cheaper than Featureflip? At three seats or fewer, yes. Once a fourth person needs a login, the whole team moves to Pro at $40 a seat and the comparison flips quickly. Five seats is $2,400 a year against $468 for Featureflip Team billed annually, which includes up to 5 team members. The honest caveat is that you are not buying the same thing: GrowthBook runs a real experiment-analysis engine on your warehouse, and Featureflip does not compute statistical significance. If experiments are central to how you work, GrowthBook earns its price. How is this estimate calculated? Seats at or below 3 are free on Starter. From the 4th seat, every seat is multiplied by the published $40 per seat per month and the total is annualized, which is why the 4th person costs $160 a month rather than $40. Above the published 50-user Pro ceiling the estimate stops and reports a quote, because GrowthBook does not publish a rate that far up. It covers Cloud seats only, so CDN request and bandwidth allowances and managed-warehouse event limits are excluded. GrowthBook publishes no annual-commit discount, so there is no billing toggle on its side. Featureflip picks the cheapest Featureflip plan that fits your seat count, a flat price that does not move with evaluations. Every rate is sourced and dated on this page. --- ## Source: https://featureflip.io/tools/launchdarkly-cost-calculator/ # LaunchDarkly Cost Calculator — Your Bill vs Flat Pricing # LaunchDarkly cost calculator LaunchDarkly bills by usage, not a flat fee. Enter your service connections and client-side MAU to estimate the Foundation bill, then see it against Featureflip's flat price. Server-side service connectionsOne server-side SDK connected to one environment for a month, so the same service in staging and production counts twice. Foundation includes the first 5.Client-side monthly active usersUnique end users your browser and mobile SDKs serve each month.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. Billing (applies to both)Annual (billed yearly)Month-to-month LaunchDarkly Foundation $4,998 per year, estimated (annual billing) Featureflip Team $468 per year, flat (annual billing). No usage-based fees on service connections and client-side MAU. At this usage, LaunchDarkly Foundation runs about **$4,998/year**, roughly **$4,530/year more** than Featureflip Team at a flat $468. LaunchDarkly's bill climbs with your usage. Featureflip's changes only when you move to another plan. LaunchDarkly rates sourced from [launchdarkly.com/pricing](https://launchdarkly.com/pricing/) , checked September 14, 2026. Estimate covers Foundation service connections and client-side MAU only; it excludes AI runs, observability (logs, traces, session replay), experimentation monthly active users, negotiated Enterprise terms and the Guardian add-on. Enterprise is quote-only, and Guardian is an Enterprise add-on priced separately. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [launchdarkly.com/pricing](https://launchdarkly.com/pricing/) , checked September 14, 2026. - **Developer** — free, up to 5 service connections and 1,000 client-side MAU. - **Foundation (annual billing)** — $10 per service connection per month beyond the 5 included, plus $8.33 per 1,000 client-side MAU per month. - **Foundation (month-to-month)** — $12 per service connection per month beyond the 5 included, plus $10 per 1,000 client-side MAU per month. - **Enterprise** — custom, quote-only. It adds code references, approval workflows, audit logging, custom roles, SCIM and more, above the Foundation figure. Guardian is an Enterprise add-on, priced separately. This estimate covers Foundation's two main flag charges. It excludes AI runs, observability, experimentation MAU, and negotiated Enterprise terms, so a real bill that uses those products is higher. ## Featureflip is flat, whatever your traffic The Free plan costs nothing, Team is $49 per month ($39 a month, $468 a year, billed annually), Pro is $99 per month ($79 a month, $948 a year, billed annually), and Business is $249 per month ($199 a month, $2,388 a year, billed annually), with no per-connection or per-MAU fees. See the [full pricing page](/pricing/) , or read the [Featureflip vs LaunchDarkly comparison](/compare/featureflip-vs-launchdarkly/) for feature parity and migration notes. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does LaunchDarkly cost? LaunchDarkly's paid Foundation plan is usage-based rather than a flat monthly fee. The first 5 service connections each month are included. On annual billing each additional server-side service connection is $10 per month, plus $8.33 per 1,000 client-side monthly active users per month. Month-to-month rates are higher, at $12 and $10. A free Developer tier covers small usage, up to 5 service connections and 1,000 client-side MAU. Enterprise is a custom, quote-only plan, and Guardian is an Enterprise add-on priced separately. Rates were checked on September 14, 2026 from launchdarkly.com/pricing. Is LaunchDarkly still priced per seat? No. LaunchDarkly moved off per-seat pricing to a usage-based model. The Foundation plan bills by service connections and client-side monthly active users, so the cost tracks your traffic and architecture rather than your headcount. That is why two teams of the same size can see very different LaunchDarkly bills. How is this estimate calculated? The calculator sets aside the 5 service connections Foundation includes each month, multiplies the rest and your client-side MAU by LaunchDarkly's published Foundation rates for the billing basis you pick, then annualizes. If your usage fits under the free Developer caps it returns zero. Featureflip's side picks the cheapest Featureflip plan that fits the team size you enter, flat, and it does not change with connections or MAU. Every rate is sourced and dated on this page. What does the calculator leave out? It estimates Foundation's two main flag-related charges: service connections and client-side MAU. It does not include LaunchDarkly's AI runs, observability such as logs, traces and session replay, experimentation MAU, or any negotiated Enterprise or Guardian terms. If you use those products your real bill is higher, which only widens the gap against a flat plan. --- ## Source: https://featureflip.io/tools/posthog-feature-flags-cost-calculator/ # PostHog Feature Flags Cost Calculator: Per-Request vs Flat # PostHog feature flags cost calculator PostHog bills feature flags by the request on a sliding curve, so your bill tracks traffic rather than team size. Enter your monthly flag requests to estimate it, then see it against Featureflip's flat price. This estimates the **feature flags line only**. PostHog is a bundle, and analytics, session replay, and surveys are metered separately. Monthly feature flag requestsClient-side flags request on page load or session start, so this tracks your monthly active users. Server-side SDKs that evaluate locally are not billed per check, but each poll for flag definitions counts as 10 requests: about 864,000 a month for one server polling every 30 seconds. Add that for each always-on instance.Team membersPeople who manage flags. Only the Featureflip side reads this: it picks the Featureflip plan that fits your team. PostHog Pay-as-you-go $5,520 per year, estimated Featureflip Pro $948 per year, flat. No usage-based fees on feature flag requests. At this usage, PostHog Pay-as-you-go runs about **$5,520/year**, roughly **$4,572/year more** than Featureflip Pro at a flat $948. PostHog's bill climbs with your usage. Featureflip's changes only when you move to another plan. PostHog rates sourced from [posthog.com/feature-flags/pricing](https://posthog.com/feature-flags/pricing) , checked September 14, 2026. Estimate covers Pay-as-you-go feature flag requests only; it excludes product analytics, session replay, surveys, and data warehouse (each metered on its own usage axis), paid platform add-ons (SSO/SAML enforcement, advanced permissions, audit controls), server-side local evaluation polling unless you add it to the input (each fetch of flag definitions counts as 10 requests, about 864,000 a month for one server polling every 30 seconds). Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [posthog.com/pricing](https://posthog.com/feature-flags/pricing) , checked September 14, 2026. - **Free** — $0, no credit card. Includes the first **1 million feature flag requests a month**, plus separate free allowances on analytics, session replay, and surveys. - **Pay-as-you-go** — the same 1 million free, then usage-based per flag request: **$0.0001** (1 to 2M), **$0.000045** (2 to 10M), **$0.000025** (10 to 50M), **$0.00001** (above 50M). Each band charges only the volume inside it. Per-product billing limits are available. - **Platform add-ons** — SSO/SAML enforcement, advanced permissions, and audit controls are paid separately. Experiments are billed together with feature flags. Two things this estimate deliberately leaves out. Every other PostHog product is metered on its own usage axis, so this is not a bundle price. And server-side local evaluation is not billed per flag check. PostHog bills the periodic fetch of flag definitions instead, at 10 flag requests each, which comes to about 864,000 requests a month for each always-on server instance at the default 30-second poll. That cost follows your instance count and poll interval, so add about 864,000 per instance to the input to include it. Per-request billing mainly affects client-side flags, where each browser session triggers a request. ## Featureflip is flat, whatever your traffic The Free plan costs nothing, Team is $49 per month ($39 a month, $468 a year, billed annually), Pro is $99 per month ($79 a month, $948 a year, billed annually), and Business is $249 per month ($199 a month, $2,388 a year, billed annually). Every plan runs unlimited evaluations, with no per-request or per-seat fees. See the [full pricing page](/pricing/) , or read the [Featureflip vs PostHog comparison](/compare/featureflip-vs-posthog/) for the bundle-versus-focused trade-off and propagation differences. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does PostHog charge for feature flags? PostHog meters feature flags by the request. The first 1 million requests a month are free, then the rate is $0.0001 per request from 1 to 2 million, $0.000045 from 2 to 10 million, $0.000025 from 10 to 50 million, and $0.00001 above 50 million. Each band is charged only for the volume that falls inside it, so a 10-million request month costs about $460, not 10 million times a single rate. There is no seat cost and no subscription price for flags. Rates were checked on September 14, 2026 from posthog.com/pricing. Does PostHog have a free tier for feature flags? Yes, and it is a generous one: 1 million flag requests every month with no credit card, alongside separate free allowances on analytics, session replay, and surveys. For a side project or a pre-launch product that is often $0 for the whole stack. The meter only becomes a budgeting question once the product works and request volume climbs. Featureflip's Free plan is also free forever, with unlimited evaluations rather than a request allowance. Do server-side feature flags cost less on PostHog? Much less, and this is the single most important thing to understand before reading any estimate. PostHog server SDKs can evaluate flags locally against cached definitions, which is not billed per flag check. The periodic fetch of those definitions is billed, at 10 flag requests each. At the default 30-second poll, one always-on server instance uses about 864,000 requests a month, 86% of the free allowance (the Go SDK polls every 5 minutes, a tenth of that). So server-side cost scales with instance count and poll interval, whatever your traffic: two instances come to about 1.73 million requests, or $72.80 a month, and ten to about 8.64 million, or $398.80. Per-request billing primarily bites client-side flags, where every browser session triggers a request. If your flags are all server-side on a single instance, PostHog may well cost you nothing. Why does my PostHog flag bill grow when my team does not? Because the meter counts traffic, not headcount. Adding engineers costs nothing, but a successful launch that doubles your monthly active users roughly doubles your client-side flag requests and therefore your flag bill. That is the structural tension in usage-based pricing: the success case and the expensive case are the same event. A flat price decouples the two, which is what this calculator is showing you. Does this estimate cover all of PostHog? No, and deliberately so. This estimates the feature-flags line only. PostHog is a bundle, and product analytics, session replay, surveys, and data warehouse are each metered on their own separate usage axis, so adding them together would compare a whole product-data stack against a focused flag tool. Paid platform add-ons such as SSO/SAML enforcement, advanced permissions, and audit controls are also excluded. Experiments are billed together with feature flags. If you already want analytics from the same vendor, PostHog's bundled value is a genuine argument this calculator does not try to price. How is this estimate calculated? Your monthly flag requests are walked through PostHog's published bands, charging each band only for the volume inside it, and the monthly total is annualized. PostHog negotiates annual plans and volume discounts through its sales team and publishes no rate for them, so there is no billing toggle on its side. Featureflip's side picks the cheapest Featureflip plan that fits the team size you enter, flat regardless of traffic. Every rate is sourced and dated on this page. --- ## Source: https://featureflip.io/tools/unleash-cost-calculator/ # Unleash Cost Calculator — Per-Seat Bill vs Flat Pricing # Unleash cost calculator Unleash cloud bills $75 per seat per month, so your bill tracks headcount. Enter your team size and monthly API requests to estimate it, then see it against Featureflip's flat price. Team members (seats)Everyone who needs an Unleash login. This is the main cost driver — Unleash cloud bills per seat from the first one.Monthly API requestsRequests your SDKs make to Unleash cloud each month. Backend SDKs evaluate locally and poll for updates, so this is far lower than your flag-check count. 53 million a month is included; beyond that it is $5 per million. Unleash Pay-As-You-Go $7,200 per year, estimated Featureflip Pro $948 per year, flat. No usage-based fees on cloud seats and API requests. At this usage, Unleash Pay-As-You-Go runs about **$7,200/year**, roughly **$6,252/year more** than Featureflip Pro at a flat $948. Unleash's bill climbs with your usage. Featureflip's changes only when you move to another plan. Unleash rates sourced from [getunleash.io/pricing](https://www.getunleash.io/pricing) , checked September 14, 2026. Estimate covers Pay-As-You-Go cloud seats and API requests only; it excludes the free self-hosted open-source edition (no license fee but you carry the operational cost), the self-hosted 5-seat minimum (it does not apply to the Cloud plan modeled here), quote-only Enterprise contract terms (a 99.99% uptime commitment, streaming through Enterprise Edge, private instances), self-hosted infrastructure and engineering time. Pay-As-You-Go already includes the full Enterprise feature set, SCIM among it. The quote-only Enterprise contract adds a 99.99% uptime commitment, streaming through Enterprise Edge, and add-ons such as private instances. Featureflip's side is the cheapest plan whose team-member and project limits fit these numbers. ## The pricing behind the numbers Sourced from [getunleash.io/pricing](https://www.getunleash.io/pricing) , checked September 14, 2026. - **Open Source** — $0 in license fees, self-hosted. Limited to 1 project and 2 environments; no SSO, custom RBAC, or change requests. AGPL-3.0 server repository. - **Pay-As-You-Go** — **$75/seat/month**, billed by credit card, after a 14-day free trial with no card. Cloud or self-hosted (5-seat minimum applies to self-hosted). Unlimited flags, projects, and environments. Includes SSO, role-based access control, and change requests. - **Cloud request allowance** — 53 million API requests a month included, then **$5 per additional million**. - **Custom Enterprise** — quote-only annual contract. Adds a 99.99% SLA, SSE streaming through Enterprise Edge (Early Access), and optional add-ons such as private instances, multi-region Edge, access logs, and IP restrictions. SCIM provisioning is already on Pay-As-You-Go, and a dedicated customer success manager comes with Premium support. This estimate covers the hosted Cloud plan. It excludes the free self-hosted open-source edition (no license fee, but you carry the operational cost), the self-hosted 5-seat minimum, and negotiated Enterprise terms. Unleash publishes no annual-commit discount on Pay-As-You-Go, so there is no annual toggle on its side. ## Featureflip is flat, whatever your headcount The Free plan costs nothing for up to 3 team members, Team is $49 per month ($39 a month, $468 a year, billed annually) for up to 5, Pro is $99 per month ($79 a month, $948 a year, billed annually) for up to 10, and Business is $249 per month ($199 a month, $2,388 a year, billed annually) for up to 25. Nothing is metered: no per-seat or per-request fees on any plan. See the [full pricing page](/pricing/) , or read the [Featureflip vs Unleash comparison](/compare/featureflip-vs-unleash/) for feature parity, open-source trade-offs, and self-hosting notes. [Start free on Featureflip](https://app.featureflip.io/signup) ## Frequently asked questions How much does Unleash cost? Unleash cloud has a single published plan: Pay-As-You-Go at $75 per seat per month, billed by credit card, after a 14-day free trial with no card required. It includes unlimited flags, projects, and environments, plus 53 million API requests a month on Cloud, with $5 per additional million beyond that. Self-hosted Pay-As-You-Go carries a 5-seat minimum. Custom Enterprise is an annual contract and quote-only. The open-source edition is free to self-host. Rates were checked on September 14, 2026 from getunleash.io/pricing. Does Unleash have a free tier? Not on cloud. Unleash cloud gives you a 14-day trial and then moves to Pay-As-You-Go at $75 per seat per month, so there is no free hosted plan to stay on. The only free option is self-hosting the open-source edition, which is limited to 1 project and 2 environments and excludes SSO, custom RBAC, and change requests. Featureflip's Free plan is free forever in the cloud with no card and no trial clock, which is why a zero-seat estimate here shows the Unleash open-source self-host case rather than a free cloud tier. Why does my Unleash bill grow when my traffic does not? Because the price is driven by headcount, not usage. Every person who needs an Unleash login costs another $75 a month, so onboarding four engineers adds $3,600 a year even if your flag count and request volume are unchanged. The 53-million-request cloud allowance is generous, and Unleash backend SDKs evaluate locally and poll for updates, so most teams never reach the overage. Seats are the variable that actually moves the bill. Can I avoid Unleash fees by self-hosting? Yes. The open-source edition has no license fee, and it is the largest open-source feature flag project on GitHub. The trade-offs are operational ownership (database, upgrades, monitoring, on-call), a 1-project and 2-environment ceiling, an AGPL-3.0 server license that some legal teams treat differently from a permissive one, and no SSO, custom RBAC, or change requests. This calculator estimates the hosted cloud plan only; self-hosting cost is your infrastructure and engineering time, which no calculator can price for you. How is this estimate calculated? Seats are multiplied by the published $75 per seat per month, then any API requests above the 53-million monthly cloud allowance are added at $5 per million, and the total is annualized. It models the Cloud plan, so the 5-seat minimum (which applies to self-hosted Pay-As-You-Go) is not imposed on the math. Unleash publishes no annual-commit discount, so there is no billing toggle on the Unleash side. Featureflip picks the cheapest Featureflip plan that fits your seat count, a flat price that does not move with requests. Every rate is sourced and dated on this page. --- ## Source: https://featureflip.io/product/features/ # Product Features [Home](/) / Features # Product features The capabilities that make up Featureflip. Each one is evaluated locally in sub-milliseconds and changes from the dashboard without a redeploy. Explore how each works in depth, then see the integrations that reach them from your editor, your Terraform, and your CI. [## Percentage rollouts Ramp a feature from 1% to 100% of users on a slider. The percentage is a dashboard setting, not a redeploy, and deterministic sticky bucketing keeps each user in place as you climb. Sticky bucketing · sub-ms local eval · live over SSE Explore the feature →](/product/features/percentage-rollouts/) [## Feature flag targeting Serve a variation to the users who match an attribute rule and the default to everyone else. Combine conditions with AND and OR, reuse a segment across flags, and change who is targeted from the dashboard. Attribute rules · reusable segments · per-environment Explore the feature →](/product/features/targeting/) [## Environments Keep development, staging, and production fully isolated. Each environment has its own SDK key and its own flag states, targeting rules, and rollout percentages, so a feature is on in one and off in another with only the key telling your deployments apart. A key per environment · isolated config · promote without a redeploy Explore the feature →](/product/features/environments/) [## Scheduled changes Queue a flag to turn on or off automatically at a future time and let Featureflip flip it for you. No cron jobs, no CI wiring, no one awake for the cutover — the change applies server-side, streams to your SDKs live, and is recorded in the audit log under your name. Future on/off · per environment · cancel while pending · Team plan and up Explore the feature →](/product/features/scheduled-changes/) [## Flag lifecycle Give each flag an expiry date and, on Pro and up, an owner. When a flag expires or stops deciding anything, a cleanup email goes out on Team and up, and a GitHub Action opens the pull request that removes its code. Expiry dates · flag owners · cleanup emails · removal by PR Explore the feature →](/product/features/flag-lifecycle/) [## Webhooks Send a signed HTTP request to your own endpoint whenever a flag or its configuration changes. Start a deploy, post to a chat channel, or refresh a cache, with retries and a delivery log behind every event. HMAC-signed · retries and redelivery · dashboard, API, MCP, Terraform Explore the feature →](/product/features/webhooks/) [## Single sign-on Let your team sign in through Okta, Microsoft Entra ID or any OpenID Connect provider. New people join with a default role, and you can require SSO so your identity provider is the only way in. OIDC · automatic provisioning · require SSO · Business plan Explore the feature →](/product/features/single-sign-on/) [## SDKs Official libraries for 13 languages and frameworks. Server SDKs evaluate flags locally in sub-milliseconds, every SDK streams live updates over SSE, and the same flag returns the same answer on every platform. 13 SDKs · local eval · one consistent API Explore the feature →](/product/features/sdks/) [## Audit log Every change to a flag, recorded automatically: who made it, what moved from and to, and when. Read it organization-wide with filters or per flag on a History tab, with retention from 7 to 365 days. Automatic · before-and-after values · filterable Explore the feature →](/product/features/audit-log/) [## REST API Everything the dashboard does, over HTTP. Create flags from CI, toggle them from a deploy script, and build internal tooling on the same configuration your team edits, with idempotent creates and rate-limit headers on every response. Full management surface · token auth · included on every plan Explore the feature →](/product/features/api/) ## Integrations Featureflip also works from the tools you already run. Each of these is included on every plan, and the cards below point straight at the docs. [### MCP server Manage flags from Claude Code, Cursor, Copilot, and other agents that speak the Model Context Protocol. The server exposes flag creation, targeting, rollouts, and staleness as tools, so the editor you are already in can create a flag and ramp it without you opening the dashboard. Claude Code · Cursor · Copilot · Cline Read the docs →](/docs/integrations/mcp/) [### Terraform provider Declare projects, flags, targeting rules, segments, and webhooks in HCL, then apply them alongside the rest of your infrastructure. Flag changes get reviewed in a pull request the same way a change to the rest of your infrastructure would be, promote cleanly between environments, and stay reproducible. On the Terraform Registry · reviewed in pull requests Read the docs →](/docs/integrations/terraform/) [### Flag cleanup Action A GitHub Action that opens one reviewable pull request per dead flag, deleting the code path across thirteen languages with AST transforms and no LLM. The same repository always produces the same diff, and merging the pull request archives the flag. 13 languages · no LLM · archives on merge Read the docs →](/docs/integrations/flag-cleanup-action/) ## Looking for the patterns, not the pieces? The use-case guides show these features working together on real problems — progressive rollouts, kill switches, entitlements, and more. The docs cover the full reference. [Browse use cases →](/use-cases/) [Read the docs →](/docs/) ## Ready to ship behind a flag? Start on the Free plan — 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/product/features/api/ # Feature Flag REST API [Home](/) / Feature flag REST API Product feature # Feature flag REST API Everything the dashboard does, over HTTP. Create flags from CI, toggle them from a deploy script, and wire flags into your own internal tools. Last updated: September 30, 2026 **The Featureflip Management API puts your whole flag configuration behind a REST interface** at `api.featureflip.io`: projects, environments, flags, variations, toggles, targeting rules, segments, and SDK keys. Requests authenticate with a bearer token, creates are idempotent so retries are safe, every response carries rate-limit headers, and every change lands in the audit log and streams live to your SDKs. It is included on every plan. [Get started free](https://app.featureflip.io/signup) [Read the API docs](/docs/management-api/overview/) ## Built for automation, not just completeness An API a script can trust: predictable addressing, safe retries, visible limits, and errors that say what went wrong and where to read about it. ### The whole surface, not a subset Everything you configure in the dashboard is available over REST: projects, environments, feature flags and their variations, per-environment configuration, on/off toggles, targeting rules, reusable segments, SDK keys, flag expiry dates, and webhook subscriptions. If the dashboard can change it, a script can too. ### Two kinds of token A Personal Access Token acts as you, with your role in each organization, which suits one-off scripts and local tooling. A Service Token is a machine identity scoped to one organization with an explicit role, and can be confined to an allowlist of projects, so your CI pipeline touches only what it should. ### Safe to retry Creates accept an Idempotency-Key header. Retry the same request after a timeout and you get the original result back instead of a duplicate flag; a concurrent retry returns a 409 rather than racing. Automation can be blunt about network failures without leaving debris behind. ### Rate limits you can see Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers, not just the ones that get throttled. A well-behaved script can pace itself before hitting 429, and when it does hit one, the response includes a Retry-After telling it exactly how long to back off. ### Errors built for scripts Every error uses one frozen JSON envelope with a stable machine-readable code, a human-readable message, and a docs_url linking to the page for that exact code. Validation failures list the offending fields; an unknown key answers with a did_you_mean suggestion. ### Addressed by the keys you already use Resources are addressed by the same keys and slugs you see in the dashboard, so a script reads like the thing it does: /orgs/acme/projects/checkout/flags/new-checkout. Lists are cursor-paginated, and every response includes an _actions map of what your token may do next. Pagination, the error envelope, rate limiting, and idempotency are specified in the [API conventions](/docs/management-api/conventions/) , and every endpoint is in the [OpenAPI reference](/docs/management-api/reference/) . ## A flag, from nothing to live, in two requests Resources are addressed by the keys you already use in the dashboard, so the calls read like what they do. Create a flag, then turn it on in one environment: ```bash # create the flag (Idempotency-Key makes a retry safe) curl -X POST https://api.featureflip.io/api/v1/orgs/acme/projects/checkout/flags \ -H "Authorization: Bearer $FEATUREFLIP_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff" \ -d '{ "key": "new-checkout", "name": "New checkout", "type": "Boolean" }' # enable it in production curl -X POST https://api.featureflip.io/api/v1/orgs/acme/projects/checkout/flags/new-checkout/environments/production/toggle \ -H "Authorization: Bearer $FEATUREFLIP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }' ``` The toggle streams to every connected [SDK](/product/features/sdks/) over SSE, the same path a dashboard change takes. The [quickstart](/docs/management-api/quickstart/) walks the full sequence from token to targeting rule, and the blog covers [managing feature flags programmatically](/blog/feature-flag-management-api/) end to end, from the write-plane and read-plane split to the automation patterns it unlocks. ## Seeding a flag from CI, step by step The most common automation: the pipeline creates the flag before the code that references it ever deploys, so the reference can never dangle. - 1 ### Create a token Generate a Personal Access Token under Settings, or a Service Token scoped to one organization and, optionally, specific projects for shared automation. Send it as a bearer token on every request. - 2 ### Create the flag POST the flag with its key, name, and type. Send an Idempotency-Key so a retried request cannot create a duplicate. The response includes the generated variations and an _actions map of what you can do next. - 3 ### Enable it where you want it A new flag starts off in every environment. Toggle it on in staging from the same script, or add a targeting rule so only your beta segment sees it in production. - 4 ### Your SDKs pick it up live The change streams to every connected SDK over SSE within moments, the same as a dashboard edit. And like a dashboard edit, it lands in the audit log with the actor attached. The same pattern covers the other direction: when a rollout completes, a cleanup job can archive the flag through the API, keeping [flag debt](/blog/feature-flag-cleanup/) from accumulating. ## One control plane, two doors The API and the dashboard drive the same configuration, so everything composes the way you would expect: - **Environments are first-class in every path.** A toggle, a rollout percentage, or a rule change is scoped to one environment, so a script can promote a flag from staging to production the same way a person would. - **Targeting is scriptable end to end.** Rules, condition groups, percentage rollouts, and reusable segments can all be created, reordered, and replaced atomically over the API. - **Every API change is in the audit log.** Writes made with a token are recorded with before and after values like any dashboard change, so automation stays as reviewable as people. - **Changes can come to you.** Create a [webhook subscription](/product/features/webhooks/) through the same API and Featureflip sends a signed request to your endpoint whenever a flag or its configuration changes, so a pipeline can react to a toggle without polling for it. - **Management and evaluation stay separate.** The Management API configures flags; your [SDKs](/product/features/sdks/) and the Evaluation API serve them. Heavy automation can never sit in the request path of a flag check. There is a door for AI agents, too. The [MCP server](/docs/integrations/mcp/) (`@featureflip/mcp`) exposes this same Management API as tools for Claude Code, Cursor, and other AI coding assistants, so an agent can create a flag, ramp a rollout, or clean up stale ones from inside the editor. The blog walks through [managing feature flags from your AI assistant](/blog/feature-flags-mcp-server/) end to end. ## Frequently asked questions Does Featureflip have a REST API for managing feature flags? Yes. The Management API at api.featureflip.io exposes everything the dashboard can do under /api/v1: create, update, archive, and delete flags, toggle them per environment, manage variations, targeting rules, segments, environments, projects, and SDK keys. It is authenticated with bearer tokens, documented with a full OpenAPI reference, and separate from the Evaluation API that serves flags to your SDKs at runtime. Can I toggle a feature flag from a script or CI pipeline? Yes. A single POST to the flag's toggle endpoint with {"enabled": true} turns it on in one environment, and the change streams to your connected SDKs over SSE just like a dashboard toggle. Teams use this to seed flags from CI before the code that references them deploys, to flip a flag as a deployment step, and to archive flags automatically once a rollout completes. How does authentication work for the Featureflip API? Every request sends a bearer token in the Authorization header. There are two kinds: a Personal Access Token acts as you with your role in each organization and is created under Settings, while a Service Token is a machine identity scoped to a single organization with an explicit role, optionally confined to an allowlist of projects. Service Tokens suit CI and shared automation because they are not tied to a person and keep working after team changes. Is the API safe to retry after a timeout? Yes. Requests that create a resource accept an Idempotency-Key header with a unique UUID per logical create. If the same token retries with the same key, the original result is replayed instead of creating a duplicate; if a second request with the same key arrives while the first is still in flight, it returns a 409 so the two cannot race. Updates and toggles set an explicit state, so repeating them is naturally harmless. How are rate limits communicated? Every response, not only throttled ones, carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers, so a script can watch its budget and pace itself. If the limit is exceeded, the API returns 429 with a stable rate_limited error code, a retry_after field, and a Retry-After header telling the client exactly how many seconds to wait before retrying. Is API access included on every plan? Yes. The Management API is available on every plan, including Free, and is not an enterprise add-on. Your plan's resource limits, such as how many flags a project can hold, apply the same through the API as through the dashboard, and flat pricing means API calls themselves are not metered or billed. ## Put your flags on autopilot The Management API is included on every plan, from Free up, on flat pricing with no metering on API calls. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Management API (docs) Base URL, authentication, and what you can manage, with links to every endpoint.](/docs/management-api/overview/) [### API quickstart (docs) From token to a live, targeted flag in a few requests.](/docs/management-api/quickstart/) [### SDKs (feature) The serving side: 13 SDKs that evaluate what the API configures, locally and in sub-milliseconds.](/product/features/sdks/) [### Audit log (feature) Every change made through the API is recorded with before and after values, like any dashboard change.](/product/features/audit-log/) --- ## Source: https://featureflip.io/product/features/audit-log/ # Feature Flag Audit Log [Home](/) / Feature flag audit log Product feature # Feature flag audit log Every change to a flag, recorded automatically. Who made it, what moved from and to, and when, so a release that goes wrong turns into a specific, attributable fact instead of a guess. Last updated: July 3, 2026 **Featureflip records every change to your flags and configuration automatically**, with nothing to enable. Each entry captures who made the change, what changed, and when, and for an edit it stores each property's previous and new value. You can read the log organization-wide, filtered by project, entity, action, user, and date, or open a single flag's History tab for a complete timeline of that flag. History is retained from 7 days on the free plan up to 365 days on Enterprise. [Get started free](https://app.featureflip.io/signup) [Read the audit log docs](/docs/guides/audit-log/) ## What the audit log gives you A complete, automatic record of every change, detailed enough to answer "what changed, and who changed it" during an incident or a compliance review. It is one part of [how Featureflip protects your data](/product/security/) . ### Every change, recorded automatically There is nothing to switch on. From the moment you start using Featureflip, every create, update, and delete across your flags and configuration is captured, so when something breaks you can answer what changed and who changed it without guesswork. ### Who, what, and when Each entry stores the actor's name and email, a precise timestamp, and the request's IP address and user agent. The actor is preserved on the entry even if that person later leaves your team, so the record stays complete for a later review. ### Before and after on every edit For an update, each changed property is recorded on its own with its previous value and its new value. You see exactly what moved, a rollout going from 25% to 100% or a flag flipping from enabled to disabled, not just that something changed. ### Everything that drives a flag The log covers feature flags, variations, targeting rules, per-environment configuration, environments, user segments, SDK keys, projects, and the organization itself. If a change can affect how a flag behaves, that change is on the record. ### Two ways to read it The organization-wide Audit Log page lists every change newest first, filterable by project, entity or entity type, action, user, and date range. Each flag also has a History tab with a complete timeline for that one flag, including its variations, per-environment config, and rules. ### Retention by plan History is kept for 7 days on Free, 30 on Team and Pro, 90 on Business, and 365 on Enterprise, and entries past your window are removed automatically. Flat pricing means the log is never metered by how many changes your team makes. For the full list of recorded entities, the filters, and the retention policy, see the [audit log documentation](/docs/guides/audit-log/) . ## What a single entry captures An entry is more than "someone edited a flag." It names the actor, the exact property that changed, and both values, so you can act on it directly. A rollout bump looks like this: Updated Per-environment configuration on `new-checkout` in Production `rolloutPercentage` 25% → 100% by Jordan Lee (jordan@acme.example) · July 3, 2026 at 14:12 UTC · 203.0.113.7 Every entry carries the same shape: the **action** (created, updated, or deleted), the **entity** it touched, the changed **property with its before and after values**, the **actor**, a **timestamp**, and the request's **IP address and user agent**. The actor is kept on the entry even if that person later leaves the team. ## Reconstructing an incident, step by step When a release goes wrong, the audit log turns "something changed" into a change you can name and undo. - 1 ### Notice behavior shifted A release starts misbehaving and you need to know what changed. Rather than guessing, open the affected flag in the Featureflip dashboard. - 2 ### Open the flag's History tab The History tab shows a complete timeline for that flag and everything beneath it: its variations, its per-environment configuration, and its targeting rules. Filter by action and date range to narrow the window you care about. - 3 ### Find the change that lines up Look for the entry whose timestamp matches when behavior shifted, a toggle, a rollout bump, or a rule edit. Because the timeline is newest first, the change that broke things is usually near the top. - 4 ### Read before and after, then act Confirm what actually changed from the previous and new values, and who changed it. Pair that with a kill switch to turn the feature off in seconds, then keep the record for the post-incident review. The audit log is one half of incident response and the [kill switch](/use-cases/kill-switch/) is the other: turn the feature off in seconds, then keep the record of who turned it on and when for the review afterward. ## The audit log sits under every change Because the log records the entities that drive flag behavior, every other capability leaves a trail without any extra step: - **Targeting edits are recorded.** Adding, reordering, or changing a rule, and creating or editing a segment, all appear on the record with their before and after values. - **Rollout moves are recorded.** Every percentage change is captured, so you can see who moved a ramp to what, and when, alongside the rest of the flag's history. - **Per-environment changes are attributed.** A toggle or configuration change is logged against the specific environment it happened in, so production changes stand apart from staging. - **Security-relevant actions are covered.** Creating, rotating, or revoking an SDK key is on the record too, so key changes are as auditable as flag changes. The actors on those entries come from your [team members and their roles](/docs/guides/team-management/) , and the whole record is delivered by the same [platform](/product/features/sdks/) your app already evaluates against. ## Frequently asked questions Does Featureflip log who changed a feature flag? Yes. Featureflip records every change automatically, with no configuration required. Each audit entry captures who made the change (name and email), what changed (the entity, and for edits the specific property with its before and after values), and when it happened, along with the request's IP address and user agent. Toggling a flag, editing a targeting rule, changing an environment configuration, creating or deleting a segment, and rotating an SDK key are all recorded. You can review the log organization-wide or scoped to a single flag from the dashboard. What changes are recorded in the audit log? Featureflip records create, update, and delete actions across feature flags, variations, targeting rules, per-environment flag configurations, environments, user segments, SDK keys, projects, and the organization itself. For updates, each changed property is recorded individually with its previous and new value. Each entry also stores the actor, a timestamp, and request metadata such as IP address and user agent, so the record covers not just flags but everything that shapes how they evaluate. Can I see the full history of a single feature flag? Yes. Every flag has a History tab that shows a complete timeline for that flag, including changes to the flag itself and to everything beneath it: its variations, its per-environment configuration, and its targeting rules. During an incident this is usually the fastest way to answer who changed this flag and when, without scrolling the organization-wide log. The per-flag history can be filtered by action and date range. How long does Featureflip keep audit logs? Audit log retention depends on your plan: 7 days on Free, 30 days on Team and Pro, 90 days on Business, and 365 days on Enterprise. Entries older than your retention window are removed automatically. If you need to keep records longer, for a compliance review or a post-incident archive, move to a plan with a longer window. Because Featureflip uses flat pricing, the audit log is not billed by the number of changes you make. Does the audit log capture the old and new values of a change? Yes. When a property is updated, Featureflip records it individually with both its previous value and its new value, so you can see exactly what moved rather than only that an edit happened. For example, a rollout entry shows the percentage going from 25% to 100%, and a toggle entry shows the state going from enabled to disabled. That before-and-after detail is what turns a vague sense that something changed into a specific, attributable fact. Can I filter or search the audit log? Yes. The organization-wide Audit Log page can be filtered by project, by entity or entity type such as a specific flag or segment, by action (created, updated, deleted), by user, and by date range, and results are paginated so a busy project stays navigable. The per-flag History tab adds action and date-range filters focused on one flag. Between the two views you can move from a broad organization-wide search down to the exact change on a single flag. ## Know who changed what, and when Audit logging is automatic on every plan, including Free with a 7-day window. Paid plans extend retention to 30, 90, or 365 days, on flat pricing with no per-seat fees. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Audit log (docs) What gets recorded, the two views and their filters, retention by plan, and the incident-review workflow.](/docs/guides/audit-log/) [### Team management (guide) Invite members and manage roles, the actors that show up on every audit entry.](/docs/guides/team-management/) [### Environments (feature) Per-environment configuration changes are attributed to the environment they happened in.](/product/features/environments/) [### Kill switch (use case) Turn a feature off fast, then read the record of who turned it on and when.](/use-cases/kill-switch/) --- ## Source: https://featureflip.io/product/features/environments/ # Feature Flag Environments [Home](/) / Feature flag environments Product feature # Feature flag environments Keep every stage of your pipeline isolated. Each environment has its own SDK key, targeting rules, and rollout percentages, so a feature can be on in development and off in production, changed from the dashboard instead of a deploy. Last updated: September 22, 2026 **Environments are isolated contexts for a flag configuration**, one per stage of your deployment pipeline. Each has its own flag states, targeting rules, rollout percentages, and SDK key, fully independent from every other environment in the project. The SDK key your app initializes with decides which environment it reads, so the same build serves Development locally and Production in production with only an environment variable between them. You promote a feature through environments as confidence grows, changing configuration from the dashboard rather than shipping code. [Get started free](https://app.featureflip.io/signup) [Read the environments docs](/docs/concepts/environments/) ## How Featureflip's environments work The pieces that keep development, staging, and production from stepping on each other, and let a feature move forward by promotion rather than a redeploy. ### Isolated configuration Every environment holds its own flag states, targeting rules, rollout percentages, and fallthrough variations. A change in one never touches another, so a flag can be fully on with aggressive rules in Development, ramping to 10 percent in Staging, and switched off in Production, all at once. That isolation is what lets you exercise a feature safely before a single real user reaches it. ### A key per environment Each environment has its own SDK key, and the key you initialize the SDK with decides which environment configuration your app receives at runtime. Use the Development key locally and the Production key in production, with nothing more than an environment variable telling them apart. Keys live outside source control, and you can regenerate one from the dashboard without disturbing the others. ### New environments start safe Add an environment and Featureflip generates a configuration for every existing flag in it, switched off. Spinning up a QA or Canary stage cannot accidentally expose a half-built feature, because nothing is on until you turn it on. The default is always the safe one. ### Promote like you promote code Move a feature forward the way code moves forward: enable and test it in Development, then Staging, then Production, widening the rollout at each step as confidence grows. Promoting the configuration is a dashboard action, not a redeploy, so the same build runs everywhere and only the environment configuration changes. ### Every layer is per-environment The on and off toggle, the targeting rules, and the percentage rollout are all set per flag per environment. Production can sit behind a conservative 5 percent ramp while Staging serves the same flag to your whole team, on one flag with no duplicated setup. Segments are shared across a project, so the audience definitions stay in one place. ### Local eval and live updates, per environment Each SDK holds one environment configuration in memory, chosen by its key, and evaluates locally in sub-milliseconds with no network round trip on the request path. Change that environment in the dashboard and the new configuration streams to the SDKs keyed to it over Server-Sent Events, typically within a second or two, and no other environment is affected. For the full model, including custom environments and SDK key rotation, see the [environments documentation](/docs/concepts/environments/) . For the step-by-step version, the guide on [managing environments](/docs/guides/environments/) walks through creating one and its SDK key. ## Setting up environments, step by step The same four moves whether you run two stages or six. The SDK key decides the environment; the configuration decides the behavior. - 1 ### Map environments to your pipeline Create an environment for each stage your code moves through. Two cover a staging-and-production setup, which is what the Free plan includes; add QA, Canary, or regional stages on the plans that allow more. Name them what your team already calls them. - 2 ### Point each deployment at its key Give every deployment the SDK key for the environment it runs in, stored in an environment variable or secrets manager. Local development gets the Development key, production gets the Production key. The key is the only thing that changes between them. - 3 ### Configure each flag per environment Set the toggle, targeting rules, and rollout percentage independently in each environment. A flag can be on for your whole team in Development and off in Production until you are ready, with no code path aware of the difference. - 4 ### Promote as confidence grows Enable the flag in the next environment up when the signal is clean, widening the rollout as you go. Each change you make streams to the SDKs on that environment in seconds, so promotion is a dashboard step rather than a release. Every flag gets one tab per environment. Switching it on in Development leaves Production exactly as it was. Want the click-by-click version in the dashboard? The guide on [creating environments and SDK keys](/docs/guides/environments/) walks through it, and the [environments concept doc](/docs/concepts/environments/) covers the recommended promotion workflow. ## What it looks like in your app Your application initializes the SDK with the key for the environment it is running in. The key decides which configuration the SDK receives, so the evaluation call below is identical in every environment, and only the key differs between deployments: ```js const client = FeatureflipClient.get({ // Development key locally, Production key in production, read from an env var sdkKey: process.env.FEATUREFLIP_SDK_KEY, }); // the environment is decided by the key above, not by this call if (client.boolVariation('new-checkout', { user_id: user.id }, false)) { return renderNewCheckout(user); } return renderCurrentCheckout(user); ``` The same call runs in every environment. Because the key selects the configuration, a flag that is on in Development and off in Production takes no branching in your code, and nothing here changes when you promote it forward. Keep keys in environment variables or a secrets manager, and pick your platform from the [SDKs](/product/features/sdks/) Featureflip publishes or the [SDK overview](/docs/sdks/overview/) . ## Environments sit above the rest of the flag An environment is the container the rest of the flag lives in. Every other capability is scoped to it: - **The toggle is per environment.** A flag on in Staging can stay off in Production until you promote it, and promoting is a dashboard change rather than a release. - **Targeting and rollouts are per environment.** One flag can run [attribute rules](/product/features/targeting/) for your team in a lower environment and a cautious [percentage ramp](/product/features/percentage-rollouts/) in production, each set independently. - **Segments are shared across the project.** An audience you define once, such as Beta Users, is available to rules in every environment, so the definition stays in one place while the rules that use it stay per environment. - **Every change is auditable.** Each environment records who changed what and when, so promoting a flag leaves a trail you can review alongside the rest of its history. The same model underpins a [staged progressive rollout](/use-cases/progressive-rollouts/) , a [beta program](/use-cases/beta-programs/) you widen environment by environment, and a [kill switch](/use-cases/kill-switch/) you can throw in production alone. For the full reference, see [how environments work](/docs/concepts/environments/) and the [guide to creating them](/docs/guides/environments/) . ## Frequently asked questions What are feature flag environments? Environments are isolated contexts for feature flag configuration. Each one keeps its own flag states, targeting rules, rollout percentages, and SDK key, completely independent from every other environment in the same project. A single flag can be fully enabled in Development, rolled out to 10 percent of users in Staging, and switched off in Production at the same time. You use environments to model the stages of your deployment pipeline, so a feature moves from local development to production the same way your code does, by promotion rather than by rewriting. How do I keep development and production flag configurations separate? Each environment has its own SDK key, and the key decides which configuration your application receives at runtime. Initialize the SDK with the Development key in local development and the Production key in your production deployment, typically by reading the key from an environment variable so the same build works in both. Because flags are configured independently per environment, a flag can be fully on in Development while it stays off in Production, and nothing in your code has to change when you promote it. Keep SDK keys in environment variables or a secrets manager, and regenerate one from the dashboard if it is ever exposed. How do SDK keys map to environments? One SDK key belongs to one environment. When your app calls the SDK, the key it was initialized with tells Featureflip which environment configuration to serve, so the key is what routes an evaluation to the right set of flag states and rules. Store each environment key in that deployment's configuration rather than in source control, and if a key leaks you can regenerate it for that one environment without affecting the others. Can a flag be on in one environment and off in another? Yes. Every flag is configured independently in each environment, including its on and off state, its targeting rules, its rollout percentage, and its fallthrough variation. That independence is the point: you enable a feature in a lower environment to test it, watch how it behaves, and leave it off in Production until you are ready to promote it. None of that requires a code change, because the differences live in the flag configuration, not in your application. How do I promote a flag from staging to production? Promotion is a configuration change in the dashboard, not a redeploy. Once a flag looks healthy in Staging, set the same state in Production, usually starting with a small percentage rollout and widening it as confidence grows. The build running in production does not change; only the Production environment configuration does, and the update streams to the SDKs keyed to that environment over Server-Sent Events within a second or two. Treating flag promotion like code promotion, moving dev to staging to production in order, keeps the process predictable. How many environments can I have, and do they cost extra? The number of environments depends on your plan, and Featureflip uses flat pricing, so environments are not billed per seat or per evaluation. The Free plan includes two, which covers a staging and production setup. Paid plans add more, up to unlimited environments on the Business plan, for teams whose pipeline has extra stages such as QA, Canary, or regional deployments. You add environments from the project settings page, and each new one starts with every flag switched off, so there is no risk of unintended exposure. ## Ship a feature through your environments Environments are in every plan, including Free: 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees, and Business includes unlimited environments. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Environments (docs) How isolation, SDK keys, and custom environments work, with the recommended promotion workflow.](/docs/concepts/environments/) [### Manage environments (guide) A step-by-step walkthrough of creating an environment, generating its SDK key, and rotating it.](/docs/guides/environments/) [### Targeting (feature) Attribute rules and reusable segments, set independently in each environment.](/product/features/targeting/) [### Percentage rollouts (feature) The per-environment ramp that holds production at 5% while staging runs at 100%.](/product/features/percentage-rollouts/) --- ## Source: https://featureflip.io/product/features/flag-lifecycle/ # Feature Flag Lifecycle Management [Home](/) / [Features](/product/features/) / Flag lifecycle Product feature # Feature flag lifecycle, from expiry date to removal Every flag gets an end date and a person who answers for it. When it is finished, they hear about it, and the pull request that deletes it is already open. Last updated: September 30, 2026 **Featureflip manages a flag's whole life, so a temporary flag leaves your codebase once it has done its job.** You give a flag an expiry date when you create it and, on Pro and above, an owner. Featureflip watches each flag's evaluation traffic every hour and marks it Stale or Dead when it stops deciding anything, or Expired once its date passes. On Team and above, the owner, or your admins if nobody owns it, gets one cleanup email. Then the flag cleanup GitHub Action opens a pull request that removes the code, and merging it archives the flag. Expiry dates, detection and the Action are on every plan, Free included. [Get started free](https://app.featureflip.io/signup) [Read the expiry dates guide](/docs/guides/flag-expiration/) ## What keeps a flag from outliving its purpose A flag nobody removes is a branch your team reads, tests and debugs around for years. These six pieces make sure each one gets a decision. ### An expiry date on the flag itself Every plan Most flags are meant to go away. Put the day you expect this one out on the flag when you create it, or later from its header. Once the date passes, the flag shows as Expired and joins the stale list. What it serves doesn't change, so a date nobody revisited can't take a feature down. ### A named owner for every flag Pro, Business and Enterprise Pick the person responsible for a flag and the flags list shows them in a column of their own. Six months later, "who can tell me whether this is safe to delete" has an answer sitting on the flag itself, one click from the list. ### A handover when someone leaves Pro, Business and Enterprise Removing a member who owns flags asks an admin who should inherit them. Their flags go to a teammate you choose, or stay unowned if you would rather sort them out later, and nothing silently points at a person who has left. ### An email when a flag is done Team, Pro, Business and Enterprise When a flag passes its expiry date or goes dead, Featureflip sends a cleanup notice. On plans with owners it goes to the flag's owner. On Team, and for any flag nobody owns, it goes to your organization's Owners and Admins. Each flag shows up once, in that day's digest. ### Detection from real traffic Every plan Every hour, Featureflip checks each flag in each environment for four traffic signals: fully rolled out, rolled back, no traffic, never used. A signal that holds for 30 days marks the flag Stale, and at 90 days it is Dead. A passed expiry date is the fifth reason, applied the moment the date goes by. ### Removal by pull request Every plan The flag cleanup GitHub Action opens one reviewable pull request per dead flag and deletes the code path across thirteen languages with AST transforms and no LLM, so the same repository always produces the same diff. Merge it and the flag is archived. The details live in the docs: [expiry dates](/docs/guides/flag-expiration/) , [flag owners](/docs/guides/flag-owners/) , [cleanup notices](/docs/guides/cleanup-notices/) , [stale flag detection](/docs/concepts/stale-flags/) and its five reasons, and the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) with its install workflow. ## One flag, start to finish A checkout redesign, say. Created on Monday, at 100% by the end of the month, and gone from the codebase a quarter later without anyone keeping a list. - 1 ### Create the flag with an end in mind Fill in Expires on when you create the flag, and on Pro and above, name its owner. Both take a few seconds and they turn "temporary" from an intention into something the platform can check. - 2 ### Ship it and ramp it Roll the feature out the usual way, with a percentage rollout or targeting rules. The expiry date and owner sit quietly on the flag and change nothing about how it evaluates. - 3 ### Featureflip notices it is finished The expiry date passes, or traffic shows the flag has served one variation to nearly everyone for 90 days. It lands in the stale list, and the owner, or your admins, get one email naming it. - 4 ### The removal arrives as a pull request The cleanup Action opens a pull request that deletes the dead code path. You review it like any other change, merge it, and the flag is archived in Featureflip. ## Built so cleanup can't break production Every piece here puts a finished flag in front of a person and stops there. An expired flag keeps serving what it served yesterday. Detection never archives, disables or deletes a flag by itself. The one automated removal step, the cleanup Action, arrives as a pull request somebody reviews and merges. - **Dates alone never trigger a removal.** A flag whose only stale reason is a passed expiry date doesn't qualify for a cleanup pull request until a traffic signal agrees, so a typo in a date can't make live code look dead. - **Permanent flags are allowed to stay.** A kill switch serving one value for a year looks finished to traffic analysis. Treat the Stale badge as a prompt to confirm the intent, then leave the [kill switch](/use-cases/kill-switch/) where it is. - **One notice per finished flag.** A cleanup email reports a flag when it expires or goes dead and then goes quiet. Nobody gets the same reminder every morning. - **Every change is on the record.** Setting, changing or clearing an expiry date or an owner is written to the [audit log](/product/features/audit-log/) like any other edit. An owner entry names the old owner and the new one, and that includes the handover when an admin removes a member. If you want the reasoning behind the whole loop, the blog walks through [feature flag cleanup](/blog/feature-flag-cleanup/) step by step, and [how automated removal works](/blog/automated-feature-flag-removal/) covers the Action's rewrites in detail. Expiry dates, owners and cleanup emails arrived together, and the [launch announcement](/blog/new-plans-sso-flag-lifecycle/) walks through all three. ## Frequently asked questions What is feature flag lifecycle management? It is the work of taking a flag from creation all the way to removal. In Featureflip that covers four things: an expiry date on the flag saying when it should come out, an owner responsible for it, stale-flag detection that notices from real traffic when a flag has stopped deciding anything, and a cleanup email plus a GitHub Action that opens the pull request removing it. Expiry dates, detection and the Action are on every plan, cleanup emails start on Team, and flag owners are on Pro and above. Does a flag stop working when its expiry date passes? No. The flag keeps serving exactly what it served the day before, with the same rules and the same rollout. An expiry date is a reminder: the flag is marked Expired, it joins the stale list with the reason PastExpiry, and on Team and above a cleanup notice goes out. If you want a flag to turn on or off at a set time, that is what scheduled changes are for. Who receives flag cleanup emails? Cleanup emails come with Team, Pro, Business and Enterprise. On Pro, Business and Enterprise, a flag's notice goes to its owner. A flag with no owner, or any flag in an organization on Team, goes to the organization's Owners and Admins. Free has no cleanup emails, though expired and dead flags still stand out in the flags list. Anyone can turn the emails off under Profile, Email Preferences, with the Flag cleanup notices switch, or with the unsubscribe link at the bottom of any notice. What happens to flag ownership if we upgrade to Pro later? Your flags arrive with owners already. On plans below Pro, Featureflip quietly records whoever created each flag as its owner, so when you upgrade the owner column is filled in rather than empty, and you only reassign the flags where the creator is the wrong person. Will an expired flag open a cleanup pull request on its own? No. A passed expiry date records intent, and intent is not proof that a flag has stopped mattering. The cleanup Action works from traffic-based removal candidates, so a flag whose only stale reason is PastExpiry waits until a traffic signal agrees. Can I set expiry dates from the API or an AI agent? Yes. The Management API has its own expiry endpoints, separate from the main flag update call, so tools that don't know about the field never clear a date by accident. The MCP server exposes a set_flag_expiry tool, and its find_stale_flags tool reports expired flags alongside the traffic-based ones. ## Give your next flag an end date Expiry dates, stale-flag detection and the cleanup Action come with every plan, including Free. Cleanup emails start on Team. Flag owners start on Pro, from $79/month billed annually. [Get started free](https://app.featureflip.io/signup) [Compare plans](/pricing/) ## Related [### Flag expiration dates (guide) Set, change or clear an expiry from the dashboard or the API, and what happens when it passes.](/docs/guides/flag-expiration/) [### Stale flag detection (docs) The five reasons a flag is marked Stale or Dead, per environment and hourly.](/docs/concepts/stale-flags/) [### Flag cleanup Action (docs) Install the GitHub Action that opens one pull request per dead flag and archives it on merge.](/docs/integrations/flag-cleanup-action/) [### Scheduled changes (feature) For a flag that should turn itself on or off at a set time.](/product/features/scheduled-changes/) --- ## Source: https://featureflip.io/product/features/percentage-rollouts/ # Percentage Rollouts [Home](/) / Percentage rollouts Product feature # Percentage rollouts Serve a feature to 1% of users, then move the number up when the metrics hold. The percentage is a setting, not a deploy, and the same users stay in as you climb. Last updated: September 22, 2026 **A percentage rollout serves a new variation to a fixed share of your users**, and lets you raise that share on a slider as the release proves out. In Featureflip the percentage is a value in the flag config: change it in the dashboard and the SDKs receive it over a streaming connection within seconds, no redeploy involved. Assignment is deterministic, so a user who is in the rollout at 5% is still in it at 50%. The result is a release you can ramp gradually, hold at any level, and reverse in a single click. [Get started free](https://app.featureflip.io/signup) [Read the rollout strategy docs](/docs/concepts/rollout-strategies/) ## How Featureflip's percentage rollouts work The mechanics that turn a percentage into a safe, reversible dial instead of a coin flip at every step. ### Deterministic sticky bucketing Featureflip hashes the user identifier together with the flag key to assign each user a stable bucket. The same user always lands in the same bucket for the same flag, so raising the percentage adds new users to the enabled cohort without re-rolling the ones already in it. Nobody flips back to the old experience mid-ramp. ### Change the percentage without a redeploy The percentage lives in the flag configuration, not in your code. Move the number in the dashboard and the SDKs pick it up on the spot. Your application binary never changes between 1% and 100%, so the deploy and the release are two separate, independently reversible events. ### Sub-millisecond local evaluation Each SDK holds the flag config in memory and computes the bucket locally. There is no network round trip on the request path and no shared flag service your hot path depends on to stay alive. Putting a percentage-gated flag in a tight loop adds no measurable latency. ### Live updates over Server-Sent Events When you change a percentage, Featureflip streams the new config to every connected SDK over its open SSE connection. Propagation is typically a second or two, fleet-wide. That is what makes ramping in tenths of a percent, or cutting straight back to zero during an incident, a practical operation rather than a redeploy cycle. ### Per-environment control A flag carries an independent percentage in every environment. Hold production at 5% while staging runs at 100%, promote the number when the signal is clean, and never leak a staging ramp into production. Each environment is its own dial. ### Multivariate weighted rollouts A rollout is not limited to on and off. Split traffic across three or more variations with the weights you choose, for example 80% control, 10% variant A, 10% variant B. The same deterministic hashing keeps each user pinned to their assigned variation as you adjust the weights. For the exact bucketing algorithm and how the fallthrough interacts with rules, see the [rollout strategies documentation](/docs/concepts/rollout-strategies/) . For the term itself, the [percentage rollout glossary entry](/glossary/percentage-rollout/) has the short version. ## Running a rollout, step by step The same four moves work for a copy tweak or a rewrite of your checkout flow. Featureflip moves the exposure; your monitoring decides the pace. - 1 ### Deploy with the flag at 0% Ship the new code path behind a flag set to zero percent. It runs on every server but executes for nobody. The deploy is a non-event: the binary is in place and no user is exposed yet. - 2 ### Raise the percentage in the dashboard Move the rollout to 1%, then 5%, then higher. Each change streams to the SDKs in seconds. A stable slice of your users starts hitting the new path, and the same slice stays enabled as you climb. - 3 ### Watch your own metrics between steps Featureflip changes the exposure; your dashboards tell you whether to keep going. Compare error rate and latency on the new path against the control, and only advance when the current tier looks healthy. - 4 ### Go to 100%, or cut back to 0% When the ramp is clean, set it to 100% and the new path becomes the only path. If a metric turns red at any step, set the percentage back to zero and the new code stops executing on the next evaluation. Rollback is a config change, not a revert. The rollout slider in the rule editor, set to 40%. Step 2 is dragging it there and clicking Update Rule. Prefer a worked example with a suggested 1% to 100% ladder and the metrics to watch at each tier? The [progressive rollouts use case](/use-cases/progressive-rollouts/) walks through one, and pairs the percentage with an instant [kill switch](/use-cases/kill-switch/) for the moment a metric turns red. ## What it looks like in your app Your code does not know it is in a rollout. It asks the SDK for the flag's value, passing the user so the bucket is stable, and branches on the answer: ```js if (client.evaluate('checkout-v2', user, false)) { return renderNewCheckout(user); } return renderLegacyCheckout(user); ``` At 0% every call returns `false`. At 5% a stable 5% of users get `true`. The code is identical at every step of the ramp. A weighted rollout is the same call with a string default, returning whichever variation the user is bucketed into: ```js // weights set in the dashboard: 80% "control", 10% "compact", 10% "spacious" const layout = client.evaluate('dashboard-layout', user, 'control'); render(layout); ``` The surface is the same in every language Featureflip supports. Pick your platform from the [SDK overview](/docs/sdks/overview/) , and see [setting up targeting rules](/docs/guides/targeting-rules/) for stacking a rule above the percentage. ## Percentage rollouts compose with everything else The percentage is one layer of the evaluation, not the whole of it. Featureflip evaluates a flag top to bottom and the rollout sits at the point where nothing more specific has matched: - **Targeting rules run first.** Give internal staff or a [named segment](/glossary/segment-targeting/) unconditional access with a rule, and let the percentage apply only to the users who fall through. That is the canary pattern and the percentage ramp working together on one flag. - **The rollout is the fallthrough.** When no rule matches, the flag serves its percentage split. Set it to 100% and the feature is fully released; set it to 0% and only your rule-targeted users see it. - **Prerequisites gate the whole thing.** If the flag depends on another flag being on, that check happens before any rollout math, so a percentage ramp never leaks a feature whose prerequisite is off. - **The percentage is per environment.** Each environment carries its own independent number, so production can hold at 5% while staging runs at 100%, and promoting the ramp forward is a dashboard change rather than a redeploy. - **Every step is auditable.** Each percentage change is recorded, so you can see who moved the rollout to what, and when, alongside the rest of your flag history. The same deterministic bucketing underpins [A/B testing](/use-cases/a-b-testing/) and [targeted rollouts](/glossary/targeted-rollout/) , so the mental model carries across all of them. ## Frequently asked questions What is a percentage rollout? A percentage rollout serves a new variation to a fixed share of your users and lets you raise that share over time. Instead of turning a feature on for everyone at once, you enable it for 1%, watch your metrics, then step up to 5%, 25%, and eventually 100%. With Featureflip the percentage is a value in the flag configuration, so changing it is a dashboard action that propagates to the SDKs in seconds rather than a code change and redeploy. Do the same users stay in the rollout as I increase the percentage? Yes. Featureflip assigns buckets by hashing the user identifier together with the flag key, which is deterministic: the same user always maps to the same bucket for that flag. When you move from 5% to 25% you are adding users to the enabled cohort, not re-randomizing the population, so the original 5% keep the feature and never flip back. This behavior is called sticky bucketing, and it is what makes a rollout safe to ramp and safe to compose with an experiment. Can I change the rollout percentage without redeploying? Yes, and that is the point of gating a rollout on a flag rather than a config file. Deploy the code once with the flag at 0%, then adjust the percentage from the Featureflip dashboard. The new value streams to every connected SDK over Server-Sent Events and takes effect on the next evaluation, usually within a second or two. Rolling back is the same operation in reverse: set the percentage to zero and the new path stops running fleet-wide. Can I roll out more than two variations by percentage? Yes. A percentage rollout is not restricted to on and off. You can split traffic across three or more variations with weights you set, such as 80% control, 10% variant A, and 10% variant B, and the weights sum to 100%. The same deterministic hashing pins each user to their assigned variation, so a user who lands in variant A stays in variant A as you adjust the split. This is a weighted, or multivariate, rollout. How is a percentage rollout different from a targeting rule? A targeting rule serves a variation to users who match an attribute you know about, for example everyone whose plan is Enterprise or whose email ends in your company domain. A percentage rollout splits the users a rule does not single out, using the deterministic hash rather than any attribute. The two compose: rules are evaluated first, and the percentage applies to whoever falls through. A common setup gives internal staff unconditional access with a rule while everyone else follows the percentage ladder. Does a percentage rollout stay consistent across servers? Yes. Because bucketing is a pure function of the user identifier and the flag key, every SDK instance computes the same assignment for the same user without coordinating. You do not need sticky sessions, a shared cache, or a central service to keep a user on one side of the split. Ten servers behind a load balancer will all agree on whether a given user is in the rollout, which is what keeps the experience consistent as requests fan out across your fleet. ## Ramp your next release behind a flag Percentage rollouts are in every plan, including Free: 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Rollout strategies (docs) The deterministic hashing algorithm, fallthrough behavior, and how rules combine with percentage splits.](/docs/concepts/rollout-strategies/) [### Progressive rollouts (use case) A worked 1% to 100% ladder, what to watch at each tier, and the pitfalls to avoid.](/use-cases/progressive-rollouts/) [### Percentage rollout (glossary) The short definition, plus how it relates to canary releases and targeted rollouts.](/glossary/percentage-rollout/) [### Kill switches Cut a feature to zero instantly when a metric turns red partway through a ramp.](/use-cases/kill-switch/) --- ## Source: https://featureflip.io/product/features/scheduled-changes/ # Scheduled Changes [Home](/) / [Features](/product/features/) / Scheduled changes Product feature # Scheduled changes Queue a flag to turn on or off at a future time and let Featureflip flip it for you. No cron jobs, no CI wiring, no one awake for the cutover. Last updated: July 3, 2026 **A scheduled change queues a feature flag to turn on or off automatically at a future time you choose.** You set the target state and the moment, in your local time, and Featureflip applies the change for you server-side, so nobody has to be online for a timed launch, an end-of-promo cutoff, or a 2am maintenance window. When it runs it behaves exactly like a manual toggle: it streams to your connected SDKs within seconds and is recorded in the audit log under your name. Schedules are set per flag per environment, and you can cancel a pending one any time before it applies. [See Team pricing](/pricing/) [Read the scheduling guide](/docs/guides/scheduled-changes/) ## How Featureflip's scheduled changes work The pieces that turn "flip this on Friday at 9am" into a change you set once and let the platform run, without a cron job or a calendar reminder. ### Turn a flag on or off at a future time Pick the target state and the moment, in your own local time, and Featureflip flips the flag for you when it arrives. A timed launch, a promo that ends on Sunday night, or a maintenance window becomes a schedule you set once and forget, instead of a calendar reminder to log in and toggle. ### Nothing to keep online The change is stored server-side and applied by Featureflip at the scheduled time. There is no cron job to write, no CI pipeline to wire up, and nobody has to be awake or at a keyboard when the moment comes. Schedule a 2am cutover from your desk in the afternoon. ### Set per environment Schedules are attached to a flag in a specific environment, so you can queue a production cutover without touching staging. Each environment carries its own independent set of scheduled changes, the same way it carries its own flag states and targeting rules. ### Cancel any time before it runs A queued change shows as Pending, with its target state, the time it will apply, and who scheduled it. Plans change, so cancel a pending schedule with one click any time before the moment it is due, and nothing happens. ### Applied exactly like a manual toggle When the time arrives, Featureflip re-runs the real change on your behalf, so the same guards, audit logging, and live streaming fire as if you had flipped the flag yourself. The audit log records it under your name, and connected SDKs pick it up over their streaming connection within seconds. ### A clear, honest history Every schedule ends in one of three states: Applied when it ran, Cancelled when you called it off, or Failed with a reason if something blocked it. A scheduled change is never silently dropped. You can always see what was queued, what happened, and why. For the click-by-click walkthrough, covering scheduling a change, watching it apply, and cancelling one, see the [scheduled changes guide](/docs/guides/scheduled-changes/) . ## Scheduling a change, step by step The same four moves cover a timed feature launch, a promotion that ends at midnight, and a maintenance window you set up the afternoon before. - 1 ### Open the flag and pick the environment Scheduled changes are per environment, so start on the environment tab where you want the change to happen: Production, Staging, or any environment in the project. What you schedule here applies only to that environment. - 2 ### Click "Schedule…" and choose the change Set the target state (Turn ON or Turn OFF) and the time it should happen, entered in your local time. Featureflip only accepts a moment in the future, so a schedule can never be queued for a time that has already passed. - 3 ### The change waits as Pending Your queued change appears in the flag's Scheduled changes panel with its target state, the time it will apply, and who created it. It sits there, doing nothing to your live flag, until the moment arrives, and you can cancel it at any point up to then. - 4 ### It applies automatically and is recorded At the scheduled time Featureflip flips the flag, streams the new state to every connected SDK, and writes the change to the audit log under your name. The entry moves to Applied, or to Failed with a reason if something prevented it, so you are never left guessing. ## What happens when a scheduled change runs A scheduled change is not a separate, lesser code path. At the chosen moment Featureflip re-runs the same flag change you would have made by hand, which means every safeguard around a normal toggle applies to a scheduled one too: - **It streams to your SDKs live.** The new flag state propagates to every connected SDK over its open [streaming connection](/docs/concepts/reliability/) within seconds, the same as any dashboard change, so your app sees the flip without a redeploy or a restart. - **It lands in the audit log under your name.** The change is recorded against the person who scheduled it, so your flag [history](/docs/guides/audit-log/) shows who queued it and when it applied, not an anonymous system entry. - **The rest of evaluation is untouched.** Turning a flag on schedules *whether* it is on, not *how* it evaluates. Your [targeting rules](/product/features/targeting/) , [percentage rollout](/product/features/percentage-rollouts/) , and [prerequisites](/docs/concepts/prerequisites/) all still apply exactly as they would after a manual flip. - **Failures are visible, not silent.** If something blocks the change when it is due, it is marked Failed with a reason rather than quietly skipped, so a scheduled cutover can never disappear without a trace. The result is a way to move a release off the clock and off your shoulders: pair a scheduled flip with a [progressive rollout](/use-cases/progressive-rollouts/) to start a ramp at a fixed time, or with a [kill switch](/use-cases/kill-switch/) to end a limited-time feature the moment it should stop. ## Frequently asked questions What are scheduled changes? A scheduled change queues a feature flag to turn on or off automatically at a future time you choose. Instead of logging in at the exact moment to flip a flag, you set the target state and the time up front, and Featureflip applies the change for you when it arrives. The change is stored server-side, so nobody needs to be online for it to run. When it applies, it behaves exactly like a manual toggle: it streams to your connected SDKs within seconds and is recorded in the audit log under the person who scheduled it. Schedules are set per flag per environment, so you can queue a production cutover without affecting staging. Can I schedule a flag to turn on and then off, like a maintenance window? Yes. You schedule each change separately. For a maintenance window you would queue one change to turn the flag off at the start and a second change to turn it back on at the end. Both sit as Pending until their times arrive and apply independently, and you can cancel either one beforehand if the plan changes. Because each side of the window is its own scheduled change, you can also move just one of them without touching the other. What happens if I am offline when a scheduled change is due? Nothing changes about how it runs. Scheduled changes are applied by Featureflip on the server at the time you chose, so they do not depend on your browser being open, your laptop being awake, or anyone being at a keyboard. You can schedule a middle-of-the-night cutover during the working day and log off. The flag flips on its own, streams to your SDKs, and lands in the audit log whether or not you are watching. Can I cancel or change a scheduled change? You can cancel any scheduled change while it is still Pending. It appears in the flag's Scheduled changes panel with a cancel action, and cancelling it means it will never apply. To change the target state or timing, cancel the pending change and schedule a new one with the details you want. Once a change has already applied, it is part of the flag's history and is shown as Applied rather than something you can undo. Flip the flag again, or schedule a new change, to move it back. Is a scheduled change audited? Yes. A scheduled change is applied by re-running the real flag change on your behalf, so it goes through the same path as a manual edit, including the audit log. The entry is recorded under the person who scheduled it, not an anonymous system account, so your flag history and Audit Logs page show who queued the change and when it took effect. That keeps the record of who changed what intact even when the change was set up hours or days in advance. Do scheduled changes still respect targeting rules and prerequisites? Yes. A scheduled change only sets whether the flag is on or off in an environment. It does not bypass the rest of evaluation. When the flag turns on, your targeting rules, segments, percentage rollout, and prerequisites all still apply at evaluation time exactly as they would after a manual toggle. Scheduling changes when a flag is evaluated, not how, so the same flag can be scheduled on and still serve its off variation to users a rule excludes. ## Schedule your next flip from your desk Set the state and the time, and Featureflip handles the cutover. Scheduled changes are on the Team plan and up, from $39/month billed annually, with nothing metered: no seats, monthly active users, or evaluation caps. [See Team pricing](/pricing/) [Get started free](https://app.featureflip.io/signup) ## Related [### Scheduled changes (guide) A step-by-step walkthrough of scheduling a change, watching it apply, and cancelling a pending one.](/docs/guides/scheduled-changes/) [### Kill switch (use case) Turn a feature off in seconds, manually now or on a schedule when a limited-time feature should end.](/use-cases/kill-switch/) [### Percentage rollouts (feature) Start a gradual ramp at a fixed time by scheduling the flag on, then climbing the slider.](/product/features/percentage-rollouts/) [### Audit log (docs) Every scheduled change is recorded under the person who queued it, with the time it applied.](/docs/guides/audit-log/) --- ## Source: https://featureflip.io/product/features/sdks/ # Feature Flag SDKs [Home](/) / Feature flag SDKs Product feature # Feature flag SDKs Official libraries for 13 languages and frameworks. Server SDKs evaluate flags locally in sub-milliseconds, every SDK streams live updates over SSE, and the same flag returns the same answer on every platform. Last updated: September 19, 2026 **Featureflip ships 13 official SDKs**, eight for server languages and five for client platforms. A server SDK holds the flag configuration in memory and evaluates the context you pass locally, so a flag check adds no network round trip and stays sub-millisecond. Every SDK keeps its configuration current over a streaming connection, and because the evaluation logic is shared and vector-tested, a given flag and context return the same variation in every language. Anything without a dedicated SDK can call the Evaluation API directly over HTTP. [Get started free](https://app.featureflip.io/signup) [Browse the SDK docs](/docs/sdks/overview/) Server SDKs: evaluate locally - [JavaScript / TypeScript](/docs/sdks/javascript/) - [Node.js](/docs/sdks/nodejs/) - [Python](/docs/sdks/python/) - [Go](/docs/sdks/go/) - [Java](/docs/sdks/java/) - [C#](/docs/sdks/csharp/) - [Ruby](/docs/sdks/ruby/) - [PHP](/docs/sdks/php/) Client SDKs: stream to the browser and mobile - [React](/docs/sdks/react/) - [Browser](/docs/sdks/browser/) - [Swift](/docs/sdks/swift/) - [Flutter](/docs/sdks/flutter/) - [Android](/docs/sdks/android/) Prefer a vendor-neutral API? [OpenFeature providers](/docs/integrations/openfeature/) wrap the Node.js, .NET, Python, Go, Java and PHP SDKs behind the [OpenFeature](https://openfeature.dev/) standard, so you can evaluate Featureflip flags through the CNCF open standard and keep your application code portable. See the [OpenFeature glossary entry](/glossary/openfeature/) for how the standard fits together, or the [provider walkthrough](/blog/openfeature-feature-flags/) for the setup in code. ## What the SDKs give you The same guarantees in every language: evaluate fast, stay current, fail safe, and get the same answer everywhere. ### Thirteen official SDKs Server libraries for JavaScript and TypeScript, Node.js, Python, Go, Java, C#, Ruby, and PHP, plus client libraries for React, the browser, Swift, Flutter, and Android. Each is a drop-in package published to the registry your stack already uses, so adding Featureflip is an install and a few lines rather than a new service to run. ### Local evaluation on the server Server SDKs hold the full flag configuration in memory and evaluate the context you pass locally, so a flag check is a function call with no network round trip on the request path. That keeps evaluation sub-millisecond and safe to call in a hot loop, and it means your app keeps serving flags even during a blip reaching Featureflip. ### Live updates over Server-Sent Events Every SDK keeps an open SSE connection and receives configuration changes as they happen, typically within a second or two, with no polling. Flip a flag or move a rollout in the dashboard and connected SDKs pick it up on the next evaluation, fleet-wide, without a redeploy. ### One consistent API The call shape is the same in every language: ask for a flag by key, pass the evaluation context, and give a default. boolVariation, stringVariation, and their siblings behave the same way across SDKs, so a pattern you learn in one service carries straight to the next. ### The same result on every platform The evaluation logic is shared across the SDKs and checked against one common set of test vectors, so a given flag and context return the same variation whether the check runs in Go on the server or Swift on a phone. Targeting, percentage bucketing, and prerequisites resolve identically everywhere. ### Fail-safe by default Every evaluation takes a default value, and the SDK returns it if the flag is missing or the service is briefly unreachable, falling back to the last configuration it held rather than failing the request. Flags degrade to a known-safe answer instead of taking your app down with them. For the full API of each library, streaming options, and configuration, start with the [SDK overview](/docs/sdks/overview/) . Each SDK has its own reference and a quickstart. For why reconnect behavior deserves as much attention as the local cache, read [the hard part of a flag outage is recovery](/blog/feature-flag-outage-recovery/) . ## Adding a flag to your app, step by step The same four moves in any language. Install, initialize with a key, evaluate with a default, and let it stream. - 1 ### Install the SDK for your stack Add the Featureflip package from your language registry, whether that is npm, PyPI, NuGet, Maven, or another. Pick a server SDK for a backend service and a client SDK for a browser or mobile app. - 2 ### Initialize it with your environment key Create the client with the SDK key for the environment the app runs in, read from an environment variable. The key decides which environment configuration the SDK receives, so the same code serves development locally and production in production. - 3 ### Evaluate a flag with a default Ask for the flag by key, pass the context that describes the user or request, and supply a default. The SDK returns the variation, evaluated locally, and the default if the flag is missing or not yet loaded. - 4 ### Let it stream updates Leave the client running as a long-lived singleton. It keeps its configuration current over SSE, so a change you make in the dashboard reaches your app in seconds without a redeploy or a polling loop. ## What it looks like in your app A server SDK is a singleton you create once and reuse. It loads the configuration in the background, keeps it current over SSE, and evaluates locally on every call. Here it is in Node: ```js // npm install @featureflip/node import { FeatureflipClient } from '@featureflip/node'; const client = FeatureflipClient.get({ sdkKey: process.env.FEATUREFLIP_SDK_KEY, }); // evaluated locally, in-memory; returns the default if anything is missing if (client.boolVariation('new-checkout', { user_id: user.id }, false)) { renderNewCheckout(user); } ``` The shape is the same in every SDK: get a client with your [environment](/product/features/environments/) key, then call a variation method with a flag key, a context, and a default. Pick your platform from the [SDK overview](/docs/sdks/overview/) , and see the [Node quickstart](/docs/quickstart/nodejs/) for the full first-flag walkthrough. ## What sub-millisecond evaluation means, and how we measured it A server SDK evaluation is a function call against a flag configuration that already sits in your process memory. Nothing is fetched. The SDK matches the context you passed against the flag's rules and segments, hashes your user key if a percentage rollout applies, resolves any prerequisite flags, and returns a variation. We time that evaluator on production traffic through our own hosted Evaluation API. The clock starts when the evaluator receives a flag, a context and the environment's segments, and it stops when a variation comes back. Loading the configuration sits outside the window on purpose, because that is a startup cost your request path pays once and never again. Across a 30-day production window ending September 19, 2026: | Evaluation completed in | Share of evaluations | | --- | --- | | Under 50 microseconds | 89% | | Under 100 microseconds | 96% | | Under 500 microseconds | 98.5% | | Under 1 millisecond | 99.5% | | Mean | 0.054 ms | That is our own traffic in production rather than a lab benchmark. The figure also does not move with how busy we are, because a dictionary lookup and a short run of comparisons over data already in memory costs the same at a hundred evaluations a day as at a million. The measurement is taken in our Evaluation API, which runs the same evaluator your server SDK runs. Every server SDK is checked against one shared set of test vectors generated from that engine, so the algorithm and the answers it gives are identical across languages. On your side it runs on your own hardware, against however many flags you have defined. The number covers evaluation and nothing else. Your application code, the network between your users and your servers, and whatever your web framework does on either side of the call are all outside it. Client SDKs in a browser or on a phone receive pre-evaluated values over their streaming connection, so a flag read there is a local lookup too, though getting those values to the device crosses the network once. ## One flag model across your whole stack The SDKs are the delivery layer for everything else Featureflip does, and they present it the same way in every language: - **Targeting and percentage rollouts resolve in the SDK.** You pass the evaluation context; the SDK applies the rules, the rollout bucketing, and any prerequisites locally, and hands back the variation. - **The environment is chosen by the key.** The SDK key you initialize with decides which environment configuration the client reads, so the same build serves development and production. - **Streaming keeps every client in sync.** A change in the dashboard reaches your Go backend and your Swift app over the same SSE mechanism, so nothing drifts between platforms. - **Front-end and back-end share one source of truth.** A client SDK in the browser and a server SDK in your API evaluate against the same configuration, so a user sees a consistent experience across both. - **The configuration itself is scriptable.** The [Management API](/product/features/api/) creates and toggles the flags your SDKs serve, so CI can seed a flag before the code that references it deploys. - **Cleanup reaches the same languages.** When a flag is finished, the [cleanup Action](/docs/integrations/flag-cleanup-action/) removes its code across TypeScript, JavaScript, Java, Go, Python, C#, PHP and Ruby, and across the mobile stack too: Kotlin, Swift and Dart. The flag leaves your Android and iOS code the same way it leaves your API, by pull request. That is what lets one flag drive a [progressive rollout](/use-cases/progressive-rollouts/) across services, a [kill switch](/use-cases/kill-switch/) that trips everywhere at once, and an [A/B test](/use-cases/a-b-testing/) whose assignment matches on the server and the client. ## Frequently asked questions Which languages and frameworks does Featureflip have SDKs for? Featureflip publishes 13 official SDKs. On the server there are libraries for JavaScript and TypeScript, Node.js, Python, Go, Java, C#, Ruby, and PHP. On the client there are libraries for React, the browser, Swift, Flutter, and Android. Anything without a dedicated SDK can integrate directly with the Evaluation API over HTTP, so no platform is left out. Do the SDKs evaluate flags locally or call an API? Server SDKs evaluate locally. Each one holds the full flag configuration in memory and matches the context you pass in process, so a flag check is a function call with no network round trip on the request path. That keeps evaluation sub-millisecond and safe to call in a hot loop. The configuration is kept current in the background over a streaming connection, so local evaluation never means stale flags. How fast is a flag evaluation, and how do you measure it? A server SDK evaluation is an in-memory function call, so it costs a function call and a handful of comparisons with no network round trip. Measured on production traffic through our hosted Evaluation API over the 30 days to September 19, 2026, about 89% of evaluations finished inside 50 microseconds, 96% inside 100 microseconds and 99.5% inside a millisecond, with a mean of 0.054 ms. That timing covers the evaluator itself: matching rules and segments, hashing the rollout bucket, and resolving prerequisites. Loading the configuration happens once at startup and sits outside the measurement. Your own SDK runs the same evaluator, checked against shared test vectors, on your hardware. How do the SDKs get updates when I change a flag? Every SDK keeps an open Server-Sent Events connection to Featureflip. When you change a flag, a targeting rule, or a rollout in the dashboard, the new configuration streams to connected SDKs within a second or two, fleet-wide, and takes effect on the next evaluation. There is no polling interval to tune and no redeploy, though server SDKs can fall back to polling if a streaming connection cannot be held. What happens if an SDK cannot reach Featureflip? The SDKs fail safe. Every evaluation call takes a default value, and the SDK returns it whenever the flag is missing or has not loaded yet. If the streaming connection drops, the SDK keeps serving from the last configuration it received while it reconnects in the background, retrying with capped exponential backoff and falling back to polling when a stream will not hold. A running SDK does not give up reconnecting, and every reconnect pulls a full configuration snapshot rather than only resubscribing to future changes, so a flag you changed or deleted while an instance was disconnected gets corrected instead of sitting stale. None of that needs an operator to restart anything, and flags degrade to a known-safe answer rather than throwing on the request path. Are flag evaluations consistent across different SDKs? Yes. The evaluation logic is shared across the SDKs and verified against one common set of test vectors, so the same flag and the same context produce the same variation in every language. Targeting rules, percentage-rollout bucketing, and prerequisite resolution all behave identically, which means a user bucketed into a variation by your Go backend gets the same variation from your Swift app. How do I get started with a Featureflip SDK? Install the package for your stack from its registry, create the client with the SDK key for your environment, and call the variation method for the flag you want. For example, in Node you run npm install @featureflip/node, initialize with FeatureflipClient.get, and call client.boolVariation with a flag key, a context, and a default. Each SDK has a quickstart in the docs that walks through installation and your first evaluation in a few minutes. Does Featureflip work with OpenFeature? Yes. Featureflip ships six OpenFeature providers: @featureflip/openfeature-node for the OpenFeature Node.js server SDK, Featureflip.OpenFeature for the OpenFeature .NET SDK, featureflip-openfeature-provider for the OpenFeature Python SDK, github.com/canopy-labs/featureflip-go-openfeature for the OpenFeature Go SDK, io.featureflip:featureflip-openfeature for the OpenFeature Java SDK, and featureflip/featureflip-openfeature for the OpenFeature PHP SDK. You get the vendor-neutral OpenFeature API — the same evaluation calls, targeting context, and tracking — while Featureflip serves your flags, so switching cost is a configuration change rather than a rewrite. Node.js, .NET, Python, Go, Java and PHP are the languages covered today and more are planned; see the OpenFeature integration guide in the docs to get started. ## Add flags to your stack today Every SDK works on the Free plan: 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat and no per-evaluation fees, however much traffic you serve. [Get started free](https://app.featureflip.io/signup) [Read the SDK docs](/docs/sdks/overview/) ## Related [### SDK overview (docs) Choose the right SDK, see how keys are used, and jump to each language reference and quickstart.](/docs/sdks/overview/) [### Environments (feature) The SDK key selects the environment, so one build serves dev, staging, and production.](/product/features/environments/) [### Targeting (feature) Pass an evaluation context and the SDK applies your attribute rules and segments locally.](/product/features/targeting/) [### Percentage rollouts (feature) Deterministic bucketing runs in the SDK, so every platform pins a user to the same variation.](/product/features/percentage-rollouts/) --- ## Source: https://featureflip.io/product/features/single-sign-on/ # Single Sign-On (SSO) for Feature Flags [Home](/) / [Features](/product/features/) / Single sign-on Product feature # Single sign-on with your identity provider Your team opens Featureflip with the work account Okta, Microsoft Entra ID or your own OIDC provider already manages. Join and leave in one place. Last updated: September 30, 2026 **Single sign-on lets your team sign in to Featureflip through the identity provider (IdP) your company already runs.** Featureflip connects over OpenID Connect, so Okta, Microsoft Entra ID and any provider with an OIDC discovery document work. Setup takes four moves: verify your email domain with a DNS record, register Featureflip in your IdP, test the round trip, activate. After that your IdP decides who gets in. New people can join automatically with a default role, and Require single sign-on makes the IdP the only way into your organization. SSO is included on the Business plan and above. [See Business pricing](/pricing/) [Read the SSO setup guide](/docs/guides/single-sign-on/) ## How single sign-on works in Featureflip Access to your flags follows the directory your company keeps anyway, from someone's first day to the afternoon they hand back their laptop. ### The identity provider you already run Featureflip connects over OpenID Connect, so Okta, Microsoft Entra ID, Google Workspace and any provider that publishes an OIDC discovery document will work. Okta and Entra ID each get a step-by-step guide with a Terraform configuration for the IdP side, if clicking through an admin console isn't how your team works. ### Your domain, proven with DNS A connection only admits people whose email address sits on a domain you've verified with a TXT record. Featureflip re-checks every verified domain once a day and emails your Owners if the record disappears. ### Tested before anyone depends on it Test connection sends you through your IdP once and reports back, without creating an account or a session. Activate stays locked until a test passes, and changing the issuer, client ID or secret later puts the connection back in draft until it passes again. ### New people arrive with a role Switch on Add new people automatically and anyone your IdP lets in joins with the default role you pick: Viewer, Member or Admin. Leave it off and only invited people get in. Someone you removed never rejoins by themselves. ### One way in Turn on Require single sign-on and a member who tries a password, Google or GitHub is sent to SSO instead. Owners keep a password sign-in for recovery, so a misconfigured IdP can never lock everyone out, and every one of those sign-ins is written to the audit log. ### Offboarding that follows your directory SSO sessions route back through your IdP on a regular cycle, so removing someone there removes their Featureflip access as well. While SSO is required, the API only accepts personal tokens created during an SSO session, which keeps an old token from outliving the person who made it. Provider walkthroughs cover [Okta](/docs/guides/sso-okta/) and [Microsoft Entra ID](/docs/guides/sso-entra-id/) , and the [single sign-on guide](/docs/guides/single-sign-on/) has the rules every provider shares, plus a troubleshooting table for each error message. ## Setting up SSO, step by step An Owner does this once, from the Single sign-on card at the bottom of Organization Settings. Admins can view the connection, run the test and manage domains along the way. - 1 ### Verify your email domain Add your domain in the Single sign-on card under Organization Settings. Featureflip gives you a TXT record to publish, and the badge flips from Pending to Verified once DNS picks it up. - 2 ### Register Featureflip in your IdP Create an OIDC web application in Okta, Entra ID or your own provider and paste in the redirect URI the card shows. Copy the issuer URL, client ID and client secret back into Featureflip and save. - 3 ### Run the test Click Test connection and sign in through your IdP once. A wrong issuer or a mistyped secret shows up here with a message that names the fix, long before a teammate hits it. - 4 ### Activate, then decide how strict to be Activate the connection and Continue with SSO works on the sign-in page for everyone on your domain. Pick a default role for new people, and switch on Require single sign-on once you want the IdP to be the only door. The Access policy section in Organization Settings. New people join as Members, and SSO is not yet required. ## Google and GitHub sign-in, and where SSO takes over Every Featureflip plan, Free included, lets people sign in with a Google or GitHub account. It spares them one more password. It also leaves the choice of account with each person, and nothing about a personal GitHub login tells Featureflip whether its owner still works for you. SSO starts at the other end, with the directory your IT team already keeps. Your IdP enforces your password and multi-factor policy, and when someone leaves, you remove them in one place and every connected tool follows. On the sign-in page the two sit together: Continue with SSO appears under the Google and GitHub buttons. Once you switch on Require single sign-on, members can still see those buttons, but only SSO opens your organization. OIDC single sign-on comes with [Business and Enterprise](/pricing/) , and Enterprise adds SAML and SCIM for teams whose procurement checklist names them. The rest of the account protections, including two-factor authentication and the [audit log](/product/features/audit-log/) , are on every plan and described on the [security page](/product/security/) . SSO launched alongside the new plans, and the [announcement post](/blog/new-plans-sso-flag-lifecycle/) covers both. ## Frequently asked questions What is single sign-on in Featureflip? Single sign-on lets your team open Featureflip with the work account your identity provider already manages. Featureflip connects to Okta, Microsoft Entra ID or any OpenID Connect provider, and your IdP decides who gets in. You verify your email domain with a DNS record, register Featureflip as an app in your IdP, test the round trip, and activate it. New people can join automatically with a default role, and you can require SSO so members can't open the organization any other way. SSO is on the Business plan and above. Is signing in with Google or GitHub the same as SSO? No. Google and GitHub sign-in are on every Featureflip plan, including Free, and they save people a password, but each person chooses which account to use and nothing ties that account to your company. SSO starts from your identity provider instead. It holds the list of who works for you, applies your multi-factor and password policy, and lets you remove someone from every connected tool in one place. With Require single sign-on switched on, Google, GitHub and password sign-ins no longer open your organization for members. Which plan includes SSO? Single sign-on through your own OIDC identity provider comes with the Business plan, from $199/month billed annually, and with Enterprise. Enterprise also covers SAML and SCIM for teams whose procurement asks for them. Every plan, Free included, has Google and GitHub sign-in and two-factor authentication. What happens if our identity provider goes down while SSO is required? Members wait for the IdP to come back, but Owners can still sign in with their Featureflip password, so someone can always reach the organization and turn enforcement off if they need to. Each of those Owner sign-ins is recorded in the audit log. Your applications are unaffected either way: SDKs authenticate with SDK keys and keep evaluating flags whatever state the sign-in page is in. Do people added through SSO get a Featureflip password? They start without one and show an SSO badge in the member list. If someone later needs a password, Forgot password on the sign-in page lets them set one. While Require single sign-on is on, that password still won't open your organization unless they're an Owner, so the IdP stays the way in. Does everyone who signs in through SSO use a seat? Yes. Each person counts toward your plan's member limit the same way an invited member does. If the organization is full, a new SSO sign-in is refused with a message saying so, and it goes through once you free a seat or move to a larger plan. ## Put Featureflip behind your own IdP Single sign-on is part of the Business plan, from $199/month billed annually for up to 25 people, and nothing is metered: no per-seat fees, no monthly active users, no evaluation caps. [See Business pricing](/pricing/) [Get started free](https://app.featureflip.io/signup) ## Related [### Single sign-on (guide) Domains, the access policy, requiring SSO, sessions and every error message you might meet.](/docs/guides/single-sign-on/) [### SSO with Okta (guide) Register Featureflip in Okta by hand or with Terraform, then assign the people who should get in.](/docs/guides/sso-okta/) [### SSO with Microsoft Entra ID (guide) The Entra app registration from the portal, the Azure CLI or Terraform.](/docs/guides/sso-entra-id/) [### Security How Featureflip protects accounts, keys and data, on every plan.](/product/security/) --- ## Source: https://featureflip.io/product/features/targeting/ # Feature Flag Targeting [Home](/) / Feature flag targeting Product feature # Feature flag targeting Serve the right variation to the right users. Match on any attribute you pass, reuse a segment across flags, and change who is targeted from the dashboard instead of a deploy. Last updated: September 22, 2026 **Feature flag targeting serves a chosen variation to the users who match a rule**, and the flag's default variation to everyone else. A rule matches attributes you pass at evaluation, such as plan, country, email, or organization, using operators like equals, in, and startsWith, and you can combine conditions with AND and OR. The rules live in the flag configuration, so you change who is targeted from the dashboard and your SDKs pick it up over a streaming connection within seconds, with no redeploy. Each SDK evaluates the context locally, in sub-milliseconds. [Get started free](https://app.featureflip.io/signup) [Read the targeting docs](/docs/concepts/targeting/) ## How Featureflip's targeting works The pieces that turn "who should see this?" into a rule you set in the dashboard and change without shipping code. ### Attribute-based rules A targeting rule matches an attribute in the user's context against a set of values and serves the variation you choose. Target on user id, email, country, plan, organization, or any custom property your app sends at evaluation. A rule can only read the attributes you pass, so richer context means more precise targeting. ### A full operator set Compare attributes with equals, contains, startsWith, endsWith, and in for membership in a list. Combine several conditions in one rule with AND or OR, so a single rule can say plan is enterprise AND country is in US or CA, without stacking extra flags to express it. ### Reusable segments Define a group once as a segment, for example Beta Users or Enterprise, then reference it by name from any flag's rules instead of copying the conditions. A segment is scoped per project and available across every environment and flag in it. Edit the definition once and every flag that uses it updates. ### Per-environment rules Targeting is set per flag per environment. Staging can serve a feature to your whole team while production targets a single pilot customer, and promoting the rule is a copy rather than a redeploy. Each environment carries its own independent set of rules. ### Sub-millisecond local evaluation Each SDK holds the rules and segments in memory and matches the context you pass locally. There is no network round trip on the request path and no shared targeting service your hot path depends on to stay up, so a targeted evaluation adds no measurable latency. ### Live updates over Server-Sent Events Change a rule and Featureflip streams the new configuration to every connected SDK over its open SSE connection, typically within a second or two, fleet-wide. Opening a feature to a new segment takes effect on the next evaluation rather than the next deploy. For the attributes, operators, and rule-ordering details in full, see the [targeting and segments documentation](/docs/concepts/targeting/) . For the terms on their own, the [segment targeting glossary entry](/glossary/segment-targeting/) and our explainer on [releasing to a chosen audience](/glossary/targeted-rollout/) have the short versions. ## Building a targeting rule, step by step The same four moves cover internal dogfooding, an enterprise-only feature, and a regional launch. Your app supplies the context; the rule decides the variation. - 1 ### Pass context on every evaluation Send the attributes that describe the user or request, such as user_id, plan, and country, with each flag evaluation. A rule can only act on an attribute your app actually includes, so decide up front what you want to target and put it in the context. - 2 ### Add a rule In the flag's environment, add a rule: pick an attribute, an operator, and the values to match, then choose the variation to serve when it matches. Add as many rules as the flag needs, and combine conditions with AND or OR inside one rule. - 3 ### Order rules and set the fallthrough Arrange rules most specific first, since the first match wins, and set the fallthrough variation for everyone no rule singles out. Drag to reorder in the dashboard until the precedence reads the way you intend it to. - 4 ### Reuse a segment and change it live Pull recurring conditions into a segment and reference it from every flag that targets the same group. When the audience changes, edit the rule or the segment in the dashboard and the update streams to your SDKs in seconds, with no redeploy. Two conditions joined by ANY, so the enterprise plan or a @beta.example.com email is enough to match. Add a second group and a user has to pass both. Want the click-by-click version in the dashboard? The guide on [setting up targeting rules](/docs/guides/targeting-rules/) walks through building one, and [using segments](/docs/guides/segments/) covers defining a group once and reusing it. ## What it looks like in your app Your code assembles the context and asks the SDK for the flag's value. The targeting logic lives in the flag configuration, not in these branches, so you change who gets what from the dashboard without touching this code: ```js const context = { user_id: user.id, plan: user.plan, country: user.country, }; if (client.boolVariation('new-dashboard', context, false)) { return renderNewDashboard(user); } return renderLegacyDashboard(user); ``` Add a rule that serves `true` when `plan` is `enterprise` and only enterprise users hit the new dashboard. The call is identical whether one rule matches or ten. A multivariate flag is the same call with a string default, returning whichever variation the matching rule serves: ```js // a rule serves "enterprise" to plan=enterprise, "pro" to plan=pro, else the fallthrough const tier = client.stringVariation('pricing-page', context, 'standard'); render(tier); ``` You assemble the context once per request and reuse it across every flag. Pick your platform from the [SDK overview](/docs/sdks/overview/) , and see [how the evaluation context feeds targeting](/docs/concepts/targeting/) for the attributes worth sending. ## Targeting composes with the rest of the flag Rules are one layer of the evaluation, not the whole of it. Featureflip evaluates a flag top to bottom, and targeting sits ahead of the percentage split: - **Rules run before the percentage rollout.** Whoever no rule matches falls through to the flag's percentage split, so a canary rule (internal staff on) and a [percentage ramp](/product/features/percentage-rollouts/) for everyone else live on one flag. - **Segments keep an audience in one place.** A named [segment](/glossary/segment-targeting/) referenced from many flags means you change a beta cohort or an enterprise list once and every flag that uses it follows. - **Prerequisites gate before any rule.** If the flag depends on another flag being on, that check happens first, so a targeting rule never serves a feature whose prerequisite is off. - **Every change is auditable.** Each rule edit is recorded, so you can see who opened a feature to which segment, and when, alongside the rest of the flag's history. The same attribute matching drives plan-tier [entitlements](/use-cases/entitlements/) , [beta-program access](/use-cases/beta-programs/) , and [A/B assignment](/use-cases/a-b-testing/) , so one targeting model carries across all of them. Rules are set independently in each [environment](/product/features/environments/) , so the same flag can target one way in staging and another in production. ## Frequently asked questions What is feature flag targeting? Feature flag targeting decides which users see which variation of a flag based on attributes you pass at evaluation, rather than turning a feature on for everyone at once. You attach one or more rules to a flag in a given environment; each rule matches an attribute in the user's context, such as plan, country, email, or organization, against a set of values and serves a chosen variation when it matches. Users no rule singles out fall through to the flag's default variation. In Featureflip the rules live in the flag configuration, so changing who is targeted is a dashboard action that streams to your SDKs in seconds, with no code change or redeploy. What attributes and operators can I target on? You target on whatever attributes your application includes in the evaluation context. Common ones are user_id, email, country, plan tier, and organization id, plus any custom property you send. Featureflip compares them with operators including equals, contains, startsWith, endsWith, and in for membership in a list. A single rule can combine several conditions with AND or OR, so you can express something like plan is enterprise AND country is in US or CA. The more attributes your app passes, the more precise your targeting can be. How are targeting rules evaluated? Rules are evaluated top to bottom and the first match wins: as soon as a user's context satisfies a rule, its variation is served and the remaining rules are skipped. Order matters, so put the most specific rules above broader ones, for example a single admin email above an entire plan tier. If no rule matches and the flag is on, Featureflip serves the fallthrough variation, which is the default experience for everyone you have not singled out. You can drag to reorder rules in the dashboard. What is a segment and when should I use one? A segment is a reusable group of users defined by attribute rules, for example a Beta Users group defined as plan equals beta OR email ends with your company domain. Instead of recreating the same conditions on every flag, you define the group once and reference it by name from any flag's rules. Update the segment in one place and every flag that references it picks up the change immediately. Segments are scoped per project and available across all of that project's environments and flags, which makes them the right tool for beta cohorts, internal dogfooding groups, and customer tiers you target repeatedly. Can I change targeting rules without redeploying? Yes. Targeting rules are configuration, not code. Add, edit, reorder, or remove a rule in the Featureflip dashboard and the new configuration streams to every connected SDK over Server-Sent Events, taking effect on the next evaluation in a second or two, fleet-wide. Your application binary never changes, so opening a feature to a new segment and closing it again are both dashboard actions rather than deploys. Does targeting add latency to evaluation? No. Each SDK holds the flag configuration, including its rules and segments, in memory and evaluates the user's context locally, so a targeted evaluation is a function call with no network round trip on your request path. That is what keeps targeting sub-millisecond and safe to call in a hot loop. Your app supplies the context, the SDK does the matching in process, and streaming keeps the rules current without you polling for changes. ## Target your next feature without a deploy Targeting rules and segments are in every plan, including Free: 10 flags, 2 environments, no credit card. Paid plans use flat pricing with no per-seat fees. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Targeting & segments (docs) Evaluation context, the operators, rule-ordering, and how segments are defined and reused.](/docs/concepts/targeting/) [### Set up targeting rules (guide) A step-by-step walkthrough of building a rule in the dashboard and ordering it correctly.](/docs/guides/targeting-rules/) [### Use segments (guide) Define a reusable group once and reference it from every flag that targets the same users.](/docs/guides/segments/) [### Percentage rollouts (feature) The fallthrough split that ramps a feature to the users your rules do not single out.](/product/features/percentage-rollouts/) --- ## Source: https://featureflip.io/product/features/webhooks/ # Feature Flag Webhooks [Home](/) / [Features](/product/features/) / Webhooks Product feature # Feature flag webhooks Tell your deploy pipeline, your chat channel or your cache the moment a flag changes. One signed request per change, retried until your endpoint answers. Last updated: September 30, 2026 **Featureflip webhooks send a signed HTTP request to your endpoint whenever a flag or its configuration changes.** Toggles, targeting rules, variations, segments, environments, projects, SDK keys and API tokens all produce events, and each subscription picks the event types, projects and environments it wants. Every delivery carries an HMAC signature, failed deliveries are retried and logged, and you can redeliver any of them. Subscriptions are managed from the dashboard, the Management API, the MCP server or Terraform, and every plan includes at least one. [Get started free](https://app.featureflip.io/signup) [Read the webhooks guide](/docs/guides/webhooks/) ## How Featureflip webhooks work Six pieces that let the rest of your stack hear about a flag change without asking the API every minute. ### Every change, sent as it happens Toggle a flag, edit a targeting rule or rotate an SDK key, and Featureflip sends your endpoint one HTTP request describing what moved. Kick off a deploy from it, post it to a chat channel, drop a cache entry, or feed your own audit store. Nothing has to poll the API on a timer. ### Signed with a secret only you hold Each delivery carries an HMAC signature computed with your subscription's signing secret. Your handler recomputes it over the raw body and throws away anything that doesn't match. The webhooks guide has verification code for Node.js and Python you can paste in. ### Secret rotation without a gap Add a new signing secret and every delivery is signed with both the old and the new one until you retire the old one. Your endpoint moves over whenever it is ready, and Featureflip refuses to retire the last active secret, so a subscription is never left unsigned. ### Only the events you asked for Pick event types, or take all of them, and narrow a subscription to particular projects and environments. A toggle in staging never has to wake up the production deploy pipeline. ### Retries, a log and redelivery A delivery your endpoint fails is retried on a backoff schedule. Each subscription keeps a log of its deliveries with status, attempt count and the last error, so you can redeliver any one by hand after a fix, or send a test event before you rely on a new endpoint. ### Managed wherever you manage flags Create subscriptions in Organization Settings, over the /api/v1 Management API, from an AI agent through the MCP server, or as a featureflip_webhook resource in Terraform. That last route puts a new receiver through the same pull request as the flags it listens to. The [webhooks guide](/docs/guides/webhooks/) covers the full event catalog, signature verification, retries, redelivery and secret rotation, with request and response shapes for every endpoint. ## What a delivery looks like One JSON object per change. Here is the body Featureflip sends when someone turns new-checkout on in production, trimmed to the fields most receivers read: ```json { "type": "flag.toggled", "occurredAt": "2026-09-23T14:32:07.412Z", "environmentId": "0199f2aa-3333-7222-8333-444455556666", "actor": { "email": "jane@example.com", "name": "Jane Doe" }, "entity": { "type": "FeatureFlag", "id": "0199f2aa-5555-7222-8333-444455556666" }, "changes": [ { "property": "IsEnabled", "from": "false", "to": "true" } ], "description": "Toggled \"new-checkout\" on in Production" } ``` - **entity** names the flag, segment or project the change belongs to, the thing you'd look up through the [REST API](/product/features/api/) . A change to something beneath it, like a targeting rule, adds a **child** naming that object. - **changes** lists each property that moved, with its old and new value, so a chat message can say exactly what happened without a second call. - **occurredAt** is when the change was made, and it is what to order by. Deliveries can arrive out of order and more than once, and the webhook-id header is stable across retries for deduplication. The same change also lands in the [audit log](/product/features/audit-log/) , so a webhook is a copy of the record you already keep, sent somewhere you choose. ## Four places to manage them, on every plan Most teams add their first subscription from the Webhooks card in Organization Settings. Scripts use the [/api/v1 webhook endpoints](/docs/guides/webhooks/#via-the-api) , which accept an Idempotency-Key on create like the rest of the public API. Agents in Claude Code, Cursor or Copilot reach the same subscriptions through four tools on the [MCP server](/docs/integrations/mcp/) , including one that sends a test event. And teams that keep flags in HCL declare a featureflip_webhook resource with the [Terraform provider](/docs/integrations/terraform/) , where rotating a secret is a featureflip_webhook_secret resource. Every plan includes webhooks. The number of endpoints grows with the plan: - **Free**: 1 webhook endpoint - **Team**: 3 webhook endpoints - **Pro**: 3 webhook endpoints - **Business**: unlimited webhook endpoints - **Enterprise**: unlimited webhook endpoints Every other limit, and what happens to extra endpoints when a plan changes, is on the [plans and limits](/docs/concepts/plans-and-limits/) page. ## Frequently asked questions What are Featureflip webhooks? Webhooks send an HTTP request to an endpoint you control whenever a flag or other configuration changes in Featureflip. Each request is a signed JSON description of one change: what changed, who changed it, the before and after values, and when it happened. Teams use them to trigger deploys, post changes to a chat channel, refresh caches, or feed an audit pipeline of their own. Deliveries that fail are retried and logged, and you can redeliver any of them by hand. How do I check that a webhook came from Featureflip? Verify its signature. Every delivery carries webhook-id, webhook-timestamp and webhook-signature headers, and the signature is an HMAC-SHA256 of the id, timestamp and raw body computed with your subscription's signing secret. Recompute it on your side and reject the request if it doesn't match. The secret is shown once, when you create the subscription or rotate it, and the webhooks guide includes working Node.js and Python examples. What happens if my endpoint is down? Featureflip retries the delivery on a backoff schedule. If every attempt fails, the delivery is marked failed and stays in the subscription's delivery log, where you can send it again once your endpoint is back. A subscription that keeps failing round after round switches itself off, and you re-enable it from the dashboard or the API, then redeliver whatever you still need. Do webhook deliveries arrive in order? Not always. Deliveries go out concurrently and at least once, so a retried event can land after a later one, and the same event can arrive twice. Deduplicate on the webhook-id header, which stays the same across retries, and order changes by the occurredAt field in the payload. Many receivers skip ordering entirely and treat each delivery as a cue to re-read current state through the API. How many webhooks can I create? It depends on the plan. Free includes 1 webhook endpoint. Team includes 3 webhook endpoints. Pro includes 3 webhook endpoints. Business includes unlimited webhook endpoints, and Enterprise is unlimited too. Only organization admins can view or manage subscriptions. Are webhooks how SDKs receive flag changes? No. SDKs keep their own streaming connection to Featureflip and pick up changes within seconds with nothing to configure. Webhooks are for the rest of your systems: the CI pipeline, the chat channel, the data warehouse, anything that should hear about a change without holding an SDK. ## Wire your first webhook in a few minutes The Free plan includes a webhook endpoint, the Management API and the MCP server, and it doesn't expire. Paid plans raise the endpoint count with nothing metered. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Outbound webhooks (guide) Event types, payload fields, signature checks in Node.js and Python, retries and rotation.](/docs/guides/webhooks/) [### REST API (feature) Create and manage webhook subscriptions from CI alongside the flags they watch.](/product/features/api/) [### Terraform provider (docs) Declare webhooks and their signing secrets in HCL, reviewed in the same pull request as your flags.](/docs/integrations/terraform/) [### Audit log (feature) The same changes, browsable in the dashboard with before and after values.](/product/features/audit-log/) --- ## Source: https://featureflip.io/product/security/ # Featureflip Security: Data, Access, and Infrastructure # Security at Featureflip Our infrastructure, the data we hold, account and key protection, and how to report a vulnerability. Feature flags sit on the request path of everything you ship, so it is fair to ask what the service behind them does with your data. This page answers that from what Featureflip runs today. Every claim below is checkable against the product, the [documentation](/docs/) , or our [privacy policy](/privacy/) . ## Where your data lives Featureflip runs on Hetzner Cloud in the European Union, and the PostgreSQL database behind it sits on the same infrastructure. Your data is stored and processed in the EU whether you sign in from Berlin, Atlanta, or Singapore. If your own compliance posture requires data to stay in the EU, that is the default here rather than a configuration step you have to find and switch on. Cloudflare fronts the public endpoints for DNS, CDN, and web application firewall. Everything is served over HTTPS. featureflip.io sends HTTP Strict Transport Security with a two-year lifetime, subdomain coverage, and preload, so a browser that has reached us once will refuse to fall back to plain HTTP even if something tries to strip the scheme. Frame, content-type, referrer, and permissions policies are set on every response. We use a small number of third-party processors and name all of them: Stripe for payments, Cloudflare, Hetzner, Resend for transactional email, Grafana Cloud for monitoring, and Google Analytics plus Microsoft Clarity for marketing-site analytics. Card details never reach our systems, because Stripe handles payment processing end to end. The [privacy policy](/privacy/) carries the current list along with what each one receives. ## What we store, and what we do not The most sensitive data a flag platform can touch is the evaluation context: the user IDs, email addresses, plan tiers, and custom attributes your application passes into a flag check so the right variation comes back. Featureflip does not store it. Server-side SDKs go a step further and never transmit it. A server SDK downloads the flag configuration once at startup and keeps it in memory, so evaluating a flag is a local lookup with no network round trip. The attributes are read inside your own process and stay there. This is the same architecture that keeps flags resolving during a Featureflip outage, described in the [reliability guide](/docs/concepts/reliability/) . Client-side SDKs work differently. A browser cannot hold your targeting rules without exposing them to anyone who opens devtools, so client SDKs send the evaluation context to us and receive pre-evaluated values back. That context is used for the evaluation and then discarded. Your rule logic, your segment definitions, and any flag you have left unmarked for client-side use all stay on our side of the wire. Beyond that we hold what you would expect an account to need: your name, email, a hashed password, your flag and targeting configuration, and the audit log. Deleting your account deactivates it immediately and removes personal data permanently after a 30-day grace period, with audit entries anonymized. ## Signing in Two-factor authentication is available on every plan, including the free one. Featureflip uses standard time-based one-time codes, so any authenticator app works. Turning 2FA on issues a set of recovery codes, each usable once, for the day your phone goes in a river. You can regenerate them whenever you want by confirming with your password, which invalidates the old set. You can also sign in with Google or GitHub and skip having a Featureflip password at all. Either way, Featureflip signs you in only from an address the provider itself reports as verified. If an account already exists under the same address, Featureflip links the two only when that email has been verified, because automatically linking an unverified account is a well-worn route to account takeover. On the Business plan an organization can [sign in through its own identity provider](/docs/guides/single-sign-on/) over OIDC, such as [Okta](/docs/guides/sso-okta/) or [Microsoft Entra ID](/docs/guides/sso-entra-id/) , and require that for every member. Enterprise plans add SAML and SCIM. The [account security guide](/docs/guides/account-security/) walks through enrollment. Passwords are stored hashed, never in plain text, and are subject to length and complexity requirements at sign-up. Repeated failed sign-in attempts lock the account temporarily, which makes online password guessing impractical without slowing down anyone who simply mistyped. Inside an organization, members hold one of three roles. Owners have full access including billing and member management, Admins manage flags, environments, segments, and members but cannot reach billing, and Members can view and edit flags and targeting rules without touching environments or team membership. Role changes and removals are themselves recorded in the audit log. ## Keys, tokens, and blast radius Featureflip issues a separate SDK key per [environment](/product/features/environments/) . A key that escapes therefore reaches development, or staging, or production, instead of all three at once. Server and client keys are separate too, and they expose different things. A server SDK key can read every flag in the project and belongs on infrastructure you control. A client SDK key is public by design, because it ships inside a browser bundle where nothing stays secret, and it returns only the flags you have explicitly marked as client-side visible, as pre-evaluated values rather than rules. Deciding which flags carry that mark is the security decision, and it is yours to make per flag. The [SDK overview](/docs/sdks/overview/) lays out the split. The [Management API](/product/features/api/) uses two token types. Personal access tokens, prefixed `ffp_`, act as you. Service tokens, prefixed `ffs_`, are machine identities scoped to one organization, carrying an explicit role and an optional allowlist of the projects they may touch. A service token is the right choice for CI and automation, because it is not tied to a person and keeps working after someone leaves the team. Tokens can carry an expiry, and revoking one takes effect immediately. Rotating any key or token is recorded, so the change shows up in the log alongside everything else. ## Every change is recorded The [audit log](/product/features/audit-log/) runs automatically, with nothing to switch on. It covers creates, updates, and deletes across flags, variations, targeting rules, per-environment configuration, environments, segments, SDK keys, projects, and the organization itself. Each entry records who acted, by name and email, when they acted, and the IP address and user agent of the request. For an edit it also records the individual property that changed together with its value before and after, which is what turns "someone touched this flag on Tuesday" into an answer you can act on during an incident review. Every flag additionally carries a History tab scoped to itself and everything beneath it. Retention depends on your plan: 7 days on Free, 30 on Team and Pro, 90 on Business, and 365 on Enterprise, with entries past the window removed automatically. Moving up a plan is what buys a longer window, so check [pricing](/pricing/) if a compliance review needs records going further back than yours holds. ## Reporting a vulnerability Email [security@featureflip.io](mailto:security@featureflip.io) . The inbox is monitored and we read every report. Include whatever gets us to a reproduction fastest: the affected endpoint, SDK, or page, the steps you took, what you expected, and what actually happened. A proof of concept helps. So does telling us how you would exploit it, because the severity of a finding usually turns on what it chains into. Two requests in return. Give us a chance to ship a fix before you publish, and please keep your testing away from anything that degrades the service or reaches data belonging to another customer. Testing against your own account and your own projects is welcome. ## Frequently asked questions ### Where does Featureflip store my data? In the European Union. Featureflip runs on Hetzner Cloud in an EU region, and the PostgreSQL database sits on the same infrastructure. Your data is stored and processed in the EU regardless of where you or your users connect from. The full list of third-party processors, covering Stripe for payments, Cloudflare for DNS and CDN, Resend for transactional email, and Grafana Cloud for monitoring, is published in our privacy policy. ### Does Featureflip store the user attributes I pass into a flag evaluation? No. Evaluation context is processed for the evaluation and is not written to storage. Server-side SDKs go further and never transmit it: they download the flag configuration at startup, hold it in memory, and evaluate locally, so a flag check involves no network call and the attributes stay inside your process. Client-side SDKs do send context, because a browser cannot hold your targeting rules without exposing them, and they receive pre-evaluated values in return. ### Does Featureflip support two-factor authentication? Yes, on every plan. Featureflip uses time-based one-time codes (TOTP), which work with Google Authenticator, 1Password, Authy, and any other standard authenticator app. Enabling 2FA issues a set of single-use recovery codes for the case where you lose your authenticator, and you can regenerate them at any time by confirming with your password. You can also sign in with Google or GitHub instead of a password, on every plan. The Business plan adds single sign-on through your own identity provider over OIDC, which an organization can make mandatory for its members. SAML and SCIM are available on the Enterprise plan. ### What happens if an SDK key leaks? The damage depends on which key, and the split is deliberate. SDK keys are issued per environment, so a leaked key reaches one environment rather than your whole project. Client SDK keys are public by design and return only the flags you have explicitly marked as client-side visible, along with pre-evaluated values rather than your targeting rules. Server SDK keys can read every flag in the project and belong on your servers only. Rotating a key takes effect immediately and is recorded in the audit log. ### Can I see who changed a feature flag? Yes. Every create, update, and delete across flags, variations, targeting rules, per-environment configuration, environments, segments, SDK keys, and projects is recorded automatically, with no configuration needed. Each entry stores the actor's name and email, a timestamp, the IP address and user agent of the request, and for edits the specific property with its value before and after. Retention runs from 7 days on Free to 365 days on Enterprise. ### How do I report a security vulnerability in Featureflip? Email security@featureflip.io with enough detail to reproduce the issue: the affected endpoint or SDK, the steps you took, and what you observed. Reports go to a monitored inbox and we read every one. Please give us a chance to ship a fix before publishing, and please do not run tests that degrade the service or touch data belonging to other customers. ## Questions we have not answered here If your team runs a security review and needs detail this page does not cover, get in touch and we will answer it directly. For anything urgent, use the security address above. [Get Started Free](https://app.featureflip.io/signup) [Contact us](/contact/) [Privacy policy](/privacy/) --- ## Source: https://featureflip.io/use-cases/ # Feature Flag Use Cases [Home](/) / Use Cases # Feature flag use cases The practical patterns teams reach for once flags are in place. How each one works, where it fits, and how to run it with Featureflip. [## Progressive rollouts Ship a new feature to 1% of users first, watch the metrics, then expand in steps. The percentage is a dashboard setting, not a redeploy, and the same users stay in the cohort as you ramp. Limit blast radius · real production signal · roll back in seconds Read the guide →](/use-cases/progressive-rollouts/) [## Feature flag kill switch Wrap a risky or expensive code path in a flag so you can disable it instantly in production. Flip it off and every SDK serves the safe fallback within seconds, with no rebuild or rollback. Mitigate in seconds · scoped to one feature · fail-safe defaults Read the guide →](/use-cases/kill-switch/) [## A/B testing Serve two variations to a sticky 50/50 split and log who saw which one. The flag delivers the experiment, holds users in their group, and records exposure; your analytics decide the winner. Sticky bucketing · exposure tracking · test becomes the rollout Read the guide →](/use-cases/a-b-testing/) [## Entitlements Gate a paid feature by plan tier with a targeting rule on the plan you pass at evaluation. Move a feature between tiers from the dashboard, override a single customer, and keep billing as the source of truth for the plan. Plan-tier gating · per-customer overrides · no deploy to re-package Read the guide →](/use-cases/entitlements/) [## Beta programs Ship the beta code with the flag off, then opt testers in, hand-pick early access, and widen the cohort from the dashboard. The flag decides who is in the beta; your feedback and analytics tools own what they think. Opt-in cohorts · named early testers · roll back in seconds Read the guide →](/use-cases/beta-programs/) [## Feature flags for AI Keep the model name, system prompt, and settings in a JSON flag. Swap models with no deploy, migrate a new one in gradually, route per tenant, and kill a bad model in seconds. The flag owns the config; your eval tools own the quality. Model kill switch · gradual migration · per-tenant routing Read the guide →](/use-cases/feature-flags-for-ai/) ## New to feature flags? Start with the concept guide for how flags, variations, environments, and the evaluation model fit together — then come back for the patterns. [What are feature flags? →](/docs/concepts/feature-flags/) ## Ready to ship behind a flag? Start on the Free plan — 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/use-cases/a-b-testing/ # A/B Testing with Feature Flags [Home](/) / [Use Cases](/use-cases/) / A/B Testing # A/B testing with feature flags Serve two variations, keep every user in a sticky bucket, and log who saw what. The flag delivers the experiment; your metrics decide the winner. Last updated: June 10, 2026 **An A/B test serves two versions of a feature to randomly assigned users and measures which one performs better on a metric you care about.** A feature flag is the delivery mechanism: a 50/50 percentage split sends each user to variation A or B by a deterministic hash of their ID, and the same user stays in the same group for the life of the test. That stickiness is what keeps the comparison clean. The flag also records an impression for every evaluation, so you have a log of who saw which variation. In short, the flag owns assignment and exposure; you bring the metric and the analysis that calls the winner. For the bare definition, see [what A/B testing is](/glossary/a-b-testing/) . [Get started free](https://app.featureflip.io/signup) [Read the rollout strategy docs](/docs/concepts/rollout-strategies/) ## Why run an A/B test behind a flag? Four reasons teams reach for a flag when they want to compare two versions in production. - 1 ### Start and stop without a deploy A test is a configuration, not a release. Set the split to 50/50 in the dashboard to begin, set it back to the control to end. The instrumented code ships once and sits dormant until you start the test on top of it, so the experiment never waits on a deploy window or a rollback. - 2 ### Sticky assignment keeps groups clean A user who flips between variation A and B on every page load pollutes both groups and ruins the result. The deterministic hash pins each user to one variation for the whole test, so the control and the treatment stay distinct populations you can actually compare. - 3 ### The test becomes the rollout Pick a winner and you rebuild nothing. Move the split to 100% on the winning variation and the A/B test turns into a progressive rollout of the version that won. One flag covers the test, the ramp, and the kill switch if that winner misbehaves later. - 4 ### Decouple the experiment from the pipeline Product and data folks can start, widen, or end a test from the dashboard with no code change and no deploy. Engineering ships the instrumented code once; the experiment's lifecycle lives in configuration from then on, where the people running it can reach it. ## What an A/B test needs, and which half a flag covers An A/B test has a delivery side and a measurement side. Feature flags own the delivery side completely. The measurement side stays with the metrics and analysis you already run. | Ingredient | Where it comes from | What it covers | | --- | --- | --- | | Random assignment | The feature flag | A percentage split sends each user to variation A or B by a deterministic hash of their ID, so the groups form at random rather than by who happened to log in first. | | Sticky bucketing | The feature flag | The same user keeps the same variation for the life of the test. Without that stickiness a user who flips between A and B pollutes both groups and the result means nothing. | | Exposure tracking | The feature flag | An impression event records which user saw which variation and when, giving you the log you join your outcomes against. | | A hypothesis and a metric | You | The outcome you expect to move (signups, checkout rate, retained sessions) and the direction that counts as a win, both decided before the first user is bucketed. | | The analysis | Your analytics stack | Joining exposure to outcomes and deciding whether the gap between the groups is a real effect or noise. This runs where your metrics already live. | That division is the whole mental model. The flag guarantees a fair, sticky split and a record of who saw what. Whether variation B actually beat variation A is a question your analytics answer, against the outcome you decided mattered. A/B testing is the simplest form of [experimentation](/glossary/experimentation/) , and the same split machinery powers richer designs as you grow into them. ## How to run an A/B test with a flag Six steps. The first four are flag configuration; the last two are yours. | Step | What you do | Detail | | --- | --- | --- | | 1 | Define the variations | Create a flag with the versions you want to compare: a control that serves today's behavior and one or more treatments. Two variations is the simplest test; the same split handles more. | | 2 | Split the traffic | Put a percentage rollout on the fallthrough. A 50/50 split for a head-to-head test, or an even share across however many variations you are comparing. | | 3 | Let bucketing hold the line | Each user is hashed to a stable bucket from their ID and the flag key, so they see the same variation on every visit for the whole test. Pass a consistent user identifier or the split cannot stay sticky. | | 4 | Record exposure | Every evaluation emits an impression event: which user saw which variation, and when. The SDK buffers these and flushes them to the events API for you. | | 5 | Measure against your hypothesis | Join the impression log to the outcome you picked up front and compare the groups in your analytics stack. The flag gave you a clean split; the metric tells you which variation won. | | 6 | Ship the winner | Move the split to 100% on the variation that won and the test becomes a rollout. Archive the losing variation so the flag does not linger as flag debt once the decision is made. | Steps two and three are the same [percentage rollout](/docs/concepts/rollout-strategies/) mechanics a gradual release uses, held at a fixed split for measurement rather than ramped. You can also stack a [targeting rule](/docs/concepts/targeting/) above the split to force internal staff onto one variation, or to hold a sensitive segment on the control while the rest of your users take part. ## How Featureflip handles the delivery side The assignment and exposure mechanics that make a test fair, sticky, and measurable, so the only open question is which variation won. - **Deterministic sticky bucketing.** Featureflip hashes the user ID with the flag key into a stable bucket, so the same user always sees the same variation for that flag. Growing or shrinking a variation re-buckets nobody who is already assigned. See the [rollout strategies docs](/docs/concepts/rollout-strategies/) for the algorithm. - **Two or more variations.** A percentage split serves a control against one treatment for a classic A/B test, or against several treatments at once when you want to compare more than two versions in a single flag. - **Forced variations and holdbacks.** Stack [targeting rules](/docs/concepts/targeting/) above the split to pin internal staff to the treatment for dogfooding, or to keep a segment on the control while everyone else is in the test. - **Impression events out of the box.** Each evaluation can emit an impression that records which user saw which variation. The SDKs buffer and flush these to the [events API](/docs/api/events/) , giving you the exposure log your analysis joins against. - **Real-time start and stop.** Change a split in the dashboard and every connected SDK picks it up over a Server-Sent Events stream within a second or two, fleet-wide, with no redeploy. Ending a test is just as immediate. - **One flag, test to rollout.** When the result is in, move the split to 100% on the winner and the test becomes a [progressive rollout](/use-cases/progressive-rollouts/) . No new flag, no rebuild, no second wiring job. ## What it looks like in your app The application asks the SDK which variation this user is in and renders accordingly. The third argument is the fallback, returned if the SDK cannot reach Featureflip, so the control doubles as the safe default: ```js // Returns 'control' or 'treatment'. 'control' is the fallback, // served if the SDK can't reach Featureflip. const variation = client.evaluate('checkout-headline-test', user, 'control'); if (variation === 'treatment') { return renderNewHeadline(user); // variation B } return renderCurrentHeadline(user); // variation A, the control ``` The split lives entirely in the dashboard. At a 50/50 setting, a stable half of identified users get `treatment` and the other half get `control`, and they hold those assignments for the run. The SDK records an impression each time, tying the user to the variation they saw. You join that exposure log to the conversion or retention metric you already track to decide the winner. The same surface works in every language the platform supports, from Python and Go to C#, Java, and Node. Pick a quickstart from the [SDK overview](/docs/sdks/overview/) . ## A/B test vs progressive rollout They run on the same bucketing machinery but answer different questions. Knowing which one you are doing keeps you from reading a ramp like an experiment. | | A/B test | Progressive rollout | | --- | --- | --- | | Question it answers | Which variation is better? | Is this change safe to expand? | | What you change | Hold a fixed split, often 50/50 | Raise the percentage in steps | | How long you hold it | Until you have enough data to call it | Until it is at 100% and stable | | What "done" looks like | A meaningful winner on your metric | The new path is the only path | | Bucketing | Deterministic and sticky | Deterministic and sticky | They compose well. Run an A/B test to find the variation that wins, then turn that winner into a [progressive rollout](/use-cases/progressive-rollouts/) to expand it safely. If the rolled-out winner ever turns sour, the same flag is its [kill switch](/use-cases/kill-switch/) . One flag, three jobs across its life. ## Common mistakes to avoid The patterns that turn a test into a result you cannot trust. Most of them are about the analysis, not the flag. ### Peeking and stopping early Checking the numbers every hour and stopping the moment they look significant inflates your false-positive rate. A split that reads like a 10% win on day one is often just noise that has not averaged out yet. Decide the sample size or the run length before you start, and hold the test until you reach it. ### Changing the split mid-test Moving from 50/50 to 70/30 partway through re-weights the groups and muddies every metric measured across the change. The users you already bucketed stay put, but the populations you are comparing no longer match. If you must adjust, treat it as a new test with a fresh start time, not a continuation of the old one. ### Evaluating without a stable identifier Percentage splits bucket on a hash of the user ID. Evaluate without a consistent identifier and the user drops to the fallback every time, so anonymous traffic and server jobs all land on the control and never really enter the test. Pass a stable ID, or scope the experiment to identified users only. ### Running without a hypothesis A test with no metric chosen up front and no expected direction turns into a fishing trip. You will find some difference somewhere, and it usually will not hold up when you look again. State the metric and what counts as a win before the first user is bucketed, so you are confirming a prediction rather than hunting for one. ### A sample too small to conclude Low-traffic features can run for weeks without reaching the volume needed to separate a real effect from noise. If you cannot gather enough exposures in a sensible window, a head-to-head test is the wrong instrument. Ship the more promising version behind a rollout and watch the broader release metrics instead. ## When a flag-driven A/B test is the wrong tool A flag makes the split easy, but not every decision is an A/B test. A few cases call for something else: - **There is not enough traffic to conclude.** If a feature cannot gather enough exposures in a reasonable window, a head-to-head test will never separate signal from noise. Ship the stronger candidate behind a rollout and watch the broader metrics instead. - **Nothing measurable moves.** An internal refactor or a copy fix that no metric responds to has nothing to compare. Ship it and move on; there is no experiment to run. - **The change cannot split per user.** Infrastructure swaps, schema migrations, and atomic cutovers do not divide cleanly across users. Reach for a [canary release](/glossary/canary-release/) or a staged migration, not an A/B test. - **The decision needs deeper experiment design.** Sequential testing, multi-armed bandits, and variance reduction are the job of a dedicated experimentation platform. Featureflip still supplies the assignment and exposure underneath that analysis; the statistics run in your experimentation or analytics layer. ## Frequently asked questions Can I A/B test with feature flags? Yes. A flag with two variations and a 50/50 percentage split is a working A/B test: it randomly assigns each user to a variation and holds them there with deterministic bucketing. Featureflip records an impression for every evaluation, so you get a log of who saw which variation. You find the winner by joining that exposure log to the conversion or retention metric you already track. What is the difference between an A/B test and a progressive rollout? Both split traffic with the same deterministic bucketing, but they answer different questions. An A/B test holds a fixed split, often 50/50, to learn which variation performs better. A progressive rollout raises the percentage in steps to expand one change safely toward 100%. Teams often test first to pick a winner, then roll that winner out. Does Featureflip calculate statistical significance? Featureflip handles the delivery side of a test: random assignment, sticky bucketing, and an impression log of who saw which variation through the events API. The significance calculation runs in your analytics or experimentation stack, where you join exposure to your outcome metric. That keeps the verdict tied to the metrics you already trust rather than a separate number to reconcile. How do users stay in the same group during a test? Featureflip hashes the user ID together with the flag key to assign a bucket. Because the hash is deterministic, the same user always lands in the same variation for that flag, on every visit and across devices. That stickiness keeps the control and treatment groups distinct, which is what makes the comparison valid. Pass a stable user identifier on every evaluation for it to hold. Can I test more than two variations at once? Yes. A percentage rollout can split traffic across more than two variations, so you can compare a control against several treatments in a single flag. Each user is still bucketed deterministically to one variation. More variations need more total traffic to reach a conclusion on any one of them, so multi-variation tests suit higher-volume features. Do I need to redeploy to start or stop a test? No. Deploy the instrumented code once with the flag serving the control, then start the test by setting the split in the dashboard. The change reaches every connected SDK over a Server-Sent Events stream within a second or two. Stopping is the same operation in reverse: set the split back to the control and the test ends on the next evaluation, with no rebuild. ## Put your next decision behind a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call: create a variation and start the split. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### A/B testing (definition) The glossary entry: the short definition and how a test differs from a percentage rollout.](/glossary/a-b-testing/) [### Progressive rollouts The same bucketing, ramped instead of held: expand the winning variation safely toward 100%.](/use-cases/progressive-rollouts/) [### Rollout strategies (docs) Deterministic hashing, fixed serving, and how percentage splits are configured.](/docs/concepts/rollout-strategies/) [### Experimentation Where A/B testing fits in the wider practice of deciding product changes from data.](/glossary/experimentation/) --- ## Source: https://featureflip.io/use-cases/beta-programs/ # Feature Flags for Beta Programs [Home](/) / [Use Cases](/use-cases/) / Beta Programs # Feature flags for beta programs and early access Ship the beta code invisibly, opt testers in, hand-pick early access, and expand the cohort from the dashboard. The flag decides who is in the beta; your feedback and analytics tools own what they think. Last updated: September 22, 2026 **A beta program gives a subset of users early access to a feature so you can learn from real usage before a full launch. A feature flag is a clean way to control who is in that subset.** You ship the beta code to production with the flag off, then a targeting rule serves it to a beta cohort: users who opted in, a segment you define, or a few named early testers. Because the cohort lives in configuration, you widen the beta, add a design partner, or end it entirely as a dashboard change rather than a deploy. What a flag does not do is gather what testers think or measure how they use the feature. Feedback lives in a feedback tool, and cohort behavior lives in your analytics stack. The flag owns the access decision; those tools own the learning. For the mechanics, see how [targeting rules and segments](/docs/concepts/targeting/) work. [Get started free](https://app.featureflip.io/signup) [Read the targeting docs](/docs/concepts/targeting/) ## Why run a beta program behind a flag? Four reasons teams reach for a flag when they want a controlled early-access group. - 1 ### Ship the code before the launch The beta feature can merge to main and deploy to production while the flag keeps it off for everyone. You decouple the deploy from the reveal, so the code is in place and tested in production long before you open the beta to a single user. - 2 ### Change the cohort without a deploy Who is in the beta lives in a targeting rule, so adding a customer, opening opt-in to everyone, or closing the beta is a dashboard edit. The application code that asks whether the beta is on for this user never changes. - 3 ### Grow it in steps, keep it sticky Start with a few design partners, then stack a percentage rollout to widen the beta as confidence grows. Sticky bucketing keeps the testers already in the beta in it, so their experience stays consistent while new users are added. - 4 ### Roll back in seconds if it breaks A beta is unfinished by definition, so things go wrong. Flip the flag off and every SDK serves the current experience within a second or two, fleet-wide, with no rebuild and no scramble to revert a deploy. ## What a beta program needs, and which half a flag covers A beta has an access side and a learning side. Feature flags own the access side completely. The learning side stays with the feedback and analytics tools you already run. | Part | Where it comes from | What it covers | | --- | --- | --- | | Who is in the beta | The feature flag | A targeting rule serves the beta variation to a "Beta users" segment, to accounts that opted in, or to hand-picked testers. Everyone else falls through to the current experience, so shipping the beta code does not expose it. | | Hand-picked early testers | The feature flag | Target a specific user or organization id above the cohort rule to give a design partner access before anyone else, or to hold one account back, without a code change. | | Whether a user opted in | Your app, or the dashboard | For a self-serve beta, your product stores the choice when a user clicks "join the beta" and passes it as an attribute the flag reads. For a small curated beta, skip the storage and manage membership as a segment or per-user targeting in the dashboard. | | Feedback and bug reports | Your feedback tool | Collecting what testers think, through surveys, an in-app widget, or a support channel, is a job for a feedback or product-research tool. A flag decides who is in the beta; it does not gather their responses. | | Analytics on beta usage | Your analytics stack | Measuring how the beta cohort behaves, through funnels, retention, or session replay, belongs to your analytics tool. The flag can emit an exposure event marking who saw the beta, but it does not chart the results. | That division is the whole mental model. The flag guarantees a fast, consistent answer to "is this user in the beta". What they think of it, and what the usage data says, are questions your feedback and analytics tools answer. Running a beta by [segment targeting](/glossary/segment-targeting/) on a cohort is the same rule machinery a [targeted rollout](/glossary/targeted-rollout/) uses, pointed at your testers instead of a launch cohort. ## How to run a beta program with a flag Six steps. The first is code; the rest are configuration you change without shipping. | Step | What you do | Detail | | --- | --- | --- | | 1 | Create the beta flag | Add a boolean flag for the beta feature with its off variation as the fallthrough, so a user sees it only when a rule puts them in the beta. | | 2 | Decide how membership works | A curated beta lives entirely in the dashboard as a segment or per-user targeting. A self-serve beta stores the opt-in in your app and passes it as an attribute on every evaluation. | | 3 | Add the access rule | Serve the on variation to a "Beta users" segment, or to users where a beta attribute is true, and leave the fallthrough off for everyone else. | | 4 | Invite early testers | Target specific user or organization ids above the cohort rule to give design partners access first, independent of the broader opt-in. | | 5 | Expand gradually | Stack a percentage rollout under the cohort rule, or widen the segment, so the beta grows from a handful of testers to a larger group as confidence builds. The same users stay in as you ramp. | | 6 | Graduate or roll back | When the feature is ready for everyone, set the fallthrough to on and retire the flag. If the beta goes wrong, flip it off and the whole cohort reverts to the current experience within a second or two, no redeploy. | Steps three and five are ordinary [targeting rules](/docs/guides/targeting-rules/) and [segments](/docs/guides/segments/) , the same building blocks an entitlement gate or a regional launch uses. Point them at a beta cohort and they run your early-access program. Order the rules so a named-tester override sits above the broad cohort rule, since the first match wins. ## How Featureflip handles the access side The rule and evaluation mechanics that turn a beta cohort into a fast, consistent access decision, so the only thing your other tools own is the feedback and the usage data. - **Cohort targeting and opt-in.** A rule serves the on variation to a "Beta users" segment or to users where a beta attribute is true. Rules run top to bottom and the first match wins, so a specific tester override can sit above the broad cohort rule. See the [targeting docs](/docs/concepts/targeting/) for operators and ordering. - **Reusable segments.** Define a "Beta users" segment once and reference it from every beta flag. Update the definition in one place and it propagates, so opening or closing the cohort is a single edit. See the [segments guide](/docs/guides/segments/) . - **Sticky gradual expansion.** Stack a percentage rollout under the cohort rule to grow the beta in steps, the way a beta ring widens in [a staged progressive delivery](/blog/progressive-delivery-explained/) . Bucketing is deterministic, so a tester who is in the beta stays in as you raise the percentage, and the cohort holds together across every replica. - **Named early testers.** Target a single organization or user id to grant a design partner access ahead of the wider beta, or hold one account back, independent of the cohort rule everyone else falls through. - **Sub-millisecond local evaluation.** The beta check runs against an in-memory copy of the config, so gating a feature on beta membership adds no network hop. The config stays local even if the control plane has a blip. - **Real-time changes.** Widen the beta, add a tester, or end it in the dashboard and every connected SDK picks up the new rule over a Server-Sent Events stream within a second or two, fleet-wide, with no redeploy. An early-access rule in the dashboard. Anyone on the enterprise plan, or signed up with a beta address, gets the feature. ## What it looks like in your app The application passes the user, and any beta opt-in, in the evaluation context and asks the flag whether the beta is on for them. The last argument is the fallback, returned if the SDK cannot reach Featureflip, so the current experience doubles as the safe default: ```js const context = { user_id: 'user-456', org_id: 'org-123', beta: user.hasJoinedBeta, // opt-in stored in your app }; // 'false' is the safe fallback: served when the user is not // in the beta, or when the SDK can't reach Featureflip. if (client.boolVariation('new-editor-beta', context, false)) { return renderNewEditor(context); } return renderCurrentEditor(context); ``` When you record which testers actually saw the beta, emit an exposure event so your analytics can segment behavior by cohort. The flag marks who was exposed; your analytics tool charts what they did: ```js // The flag decides access and records the exposure. // Your analytics stack owns the funnels and retention. if (client.boolVariation('new-editor-beta', context, false)) { analytics.track('beta_exposed', { feature: 'new-editor' }); } ``` The cohort rules live entirely in the dashboard. Widen `new-editor-beta` from a handful of design partners to everyone who opted in, or graduate it to on for all users, and every SDK reflects it on its next evaluation without a rebuild. Because the fallback is the current experience, an outage fails safe to the existing UI rather than exposing an unfinished feature. The same surface works in every language the platform supports, from Python and Go to C#, Java, and Node. Pick a quickstart from the [SDK overview](/docs/sdks/overview/) . ## Beta access vs beta feedback A flag covers the access side. A feedback or analytics tool covers the learning side. Knowing which one you need keeps you from asking a flag to do a research tool's job. | | Beta access (a flag) | Beta feedback (research tools) | | --- | --- | --- | | Question it answers | Who is in the beta, and what do they see? | What do beta users think, and how are they using it? | | What it owns | The access decision and gradual expansion | Feedback capture, surveys, and product analytics | | Source of truth | Reads the opt-in or segment you target | Owns the responses and the usage data | | How it changes | Flip a rule or widen the cohort in the dashboard | Feedback flows in from testers over time | | Right tool | A feature flag | A feedback, research, or analytics tool | They compose. The flag is the switch that controls exposure inside your application, and the feedback tool owns the responses that come back. If all you need is to give a group early access and be able to pull it, a flag is the whole answer. If you also need to collect structured feedback and measure the cohort, pair the flag with a research or analytics tool and let the flag decide who is in the beta in front of it. ## Common mistakes to avoid The patterns that turn a clean beta into a mess. Most of them come from asking the flag to own something a feedback or analytics tool should. ### Treating the flag as your feedback inbox A flag decides who is in the beta; it does not collect what they think. Trying to track bug reports or survey answers by toggling flags means something else is doing the real work anyway. Let the flag gate access and pair it with a feedback or research tool that owns the responses, so each side does the job it is built for. ### Non-sticky beta assignment If a tester's inclusion is recomputed randomly on each request, they flip in and out of the beta between page loads, which makes their feedback meaningless. Assign the cohort with sticky bucketing so a user who is in the beta stays in it every time, until you explicitly change the rule. ### Forgetting the safe default The fallback passed to the evaluation is what the SDK returns if it cannot reach Featureflip. For a beta that value should be off, the current experience, so an outage never drops your whole user base into an unfinished feature. Default to off and let a matched rule opt a user into the beta, never the other way around. ### Leaving the beta flag in place forever Once a feature graduates and everyone has it, the beta flag is dead weight, an inventory item that carries a cost. Set the fallthrough to on, remove the now-redundant rule, and delete the flag. A permanent beta flag is exactly the kind of flag debt a cleanup routine exists to catch. ### Hand-targeting a large beta one id at a time Adding hundreds of individual user ids to a rule does not scale and turns every change into a chore. Define a segment for your beta cohort, or gate on an opt-in attribute your app sets, and manage membership through that instead. Reserve per-user targeting for the handful of named early testers. ## When a flag-driven beta is the wrong tool A flag makes early access easy, but it is not a research platform. A few cases call for something more: - **You need to collect and triage feedback.** Surveys, in-app feedback widgets, and bug intake are a feedback or research tool's job. The flag gates access; it does not gather or organize what testers say. Run both and let each own its half. - **You need a public waitlist and signup funnel.** Capturing interest, sending confirmation emails, and managing a queue belong to a marketing or CRM tool. A flag only gates the feature once a user is admitted; it does not run the funnel that gets them there. - **You want a statistically valid comparison.** If the goal is to prove one variant beats another with significance rather than to gather early feedback, that is an experiment. See [A/B testing with feature flags](/use-cases/a-b-testing/) , where the flag delivers a sticky split and your analytics call the winner. - **You need deep analytics on the cohort.** Funnels, retention curves, and session replay for beta users live in an analytics stack. The flag can emit an exposure event marking who saw the beta, but it does not model or chart their behavior. ## Frequently asked questions Can I use feature flags for beta programs? Yes. A targeting rule that serves the beta variation to a cohort, whether that is a "Beta users" segment, accounts that opted in, or a few hand-picked testers, is a complete beta-access mechanism. Everyone outside the cohort falls through to the current experience, so the beta code can ship to production without exposing the feature. You expand the cohort, add early testers, or end the beta from the dashboard, with no deploy. The flag owns who is in the beta; your feedback and analytics tools own what testers think and how they use it. How do I let users opt into a beta? Add a "join the beta" control in your product that records the choice, then pass it as an attribute, for example beta: true, in the evaluation context alongside the user id you already send. A targeting rule serves the on variation when that attribute is set, and leaving the beta is the same in reverse. For a small curated beta you can skip the app-side storage entirely and manage the cohort as a segment or per-user targeting in the Featureflip dashboard. What is the difference between a feature flag and a beta feedback tool? A feature flag makes the access decision: who is in the beta and what they see. A feedback or research tool captures the other half: what testers think, the bugs they hit, and how they use the feature. The two compose. The flag is the switch that controls exposure inside your application, and the feedback tool owns the responses that come back. Many beta programs need only the access side, and a flag covers that completely; add a feedback tool when you want to gather and triage what testers tell you. How do I expand a beta gradually? Stack a percentage rollout under the cohort rule, or widen the segment that defines the beta. A percentage rollout uses sticky bucketing, so the testers already in the beta stay in as you raise the number, and new users are added in steps rather than all at once. That lets you start with a handful of accounts, watch how the feature holds up, and grow the cohort only when the signal looks healthy, all as dashboard changes rather than deploys. How do I end a beta or roll it back? To graduate the beta, set the flag's fallthrough to on so everyone gets the feature, then remove the now-redundant cohort rule and retire the flag. To roll back, flip the flag off: every connected SDK serves the fallback, and the whole cohort reverts to the current experience within a second or two over the stream, with no redeploy. Because the fallback is off, an outage also fails safe to the non-beta experience rather than exposing an unfinished feature. Can I run a private beta for specific customers? Yes. Target specific organization or user ids above the cohort rule so they match first and get the beta regardless of the broader opt-in. That gives a named design partner or a contracted customer early access without touching the rule everyone else falls through. Removing the override later is a dashboard change, and every SDK picks it up over the stream within a second or two. ## Run your next beta behind a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call: create a flag and target a beta cohort. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### User targeting & segments (docs) Attributes, operators, and rule ordering: the mechanism a beta cohort is built on.](/docs/concepts/targeting/) [### Progressive rollouts Stack a percentage rollout under the cohort rule to widen the beta in steps as confidence grows.](/use-cases/progressive-rollouts/) [### A/B testing When the beta is really an experiment: a sticky split with the winner decided in your analytics.](/use-cases/a-b-testing/) [### Targeted rollout The glossary definition: releasing a feature to a specific cohort before a wider launch.](/glossary/targeted-rollout/) --- ## Source: https://featureflip.io/use-cases/entitlements/ # Feature Flags for Entitlements [Home](/) / [Use Cases](/use-cases/) / Entitlements # Feature flags for entitlements and plan gating Serve a paid feature to the tiers entitled to it, change who gets it from the dashboard, and keep your billing system as the source of truth for the plan. The flag enforces access; billing owns the plan. Last updated: June 30, 2026 **An entitlement is the rule that decides which customers get access to a feature, usually by plan tier. A feature flag is a clean way to enforce it.** You pass the customer's plan as an attribute at evaluation, and a targeting rule serves the paid variation to the tiers entitled to it while everyone else falls through to off. Because the rule lives in configuration, moving a feature between tiers is a dashboard change rather than a deploy, and you can override a single customer to grant early access. What a flag does not do is manage the subscription itself: which plan a customer is on, whether it is current, and how much of a quota they have used all stay in your billing system. The flag owns the access decision; billing owns the plan. For the mechanics, see how [targeting rules and segments](/docs/concepts/targeting/) work. [Get started free](https://app.featureflip.io/signup) [Read the targeting docs](/docs/concepts/targeting/) ## Why gate entitlements behind a flag? Four reasons teams reach for a flag when they want features to switch on by plan. - 1 ### Change access without a deploy Packaging changes as the product grows: a feature moves from enterprise-only into pro, or a new tier appears. When the entitlement lives in a targeting rule, that is a dashboard edit. The application code that asks whether the feature is on never changes, so re-packaging does not wait on a release. - 2 ### Gate on context you already pass Plan tier is just an attribute in the evaluation context, next to the user and organization id you already send. A rule matches on it directly, and a segment groups your paid tiers once so every gated feature can reference the same definition instead of repeating it. - 3 ### Grant early, or roll out gradually Override a single account to give a customer early access ahead of their tier, or stack a percentage rollout under the tier rule to release a new premium feature to a slice of paid users first. The same flag handles the entitlement and the gradual release together. - 4 ### Fast enough for the hot path An entitlement check runs on nearly every request that touches a gated feature. The SDK evaluates from an in-memory copy of the config, so the check adds no network hop and no measurable latency. Gating premium features does not slow the requests that use them. ## What entitlements need, and which half a flag covers Entitlements have an enforcement side and a management side. Feature flags own the enforcement side completely. The management side stays with the billing system you already run. | Part | Where it comes from | What it covers | | --- | --- | --- | | The access rule per tier | The feature flag | A targeting rule keyed on the plan attribute serves the paid variation to the tiers entitled to it and the off variation to everyone else. Reorder the tiers or add one from the dashboard. | | Per-customer overrides | The feature flag | Target a single organization or user above the tier rule to grant a feature ahead of its plan, honor a contract, or hold one account back, without a code change. | | The plan each customer is on | Your billing system | The flag reads the plan you pass at evaluation. Which plan a customer is on, and whether their subscription is current, stays authoritative in the billing system you already run. | | Usage metering and quotas | Your billing or usage system | Counting API calls, seats, or events against a plan limit is a job for a metering system. A flag makes a yes-or-no decision; it does not tally usage or enforce a running total. | | The upgrade flow | Your billing or paywall | A flag gates the locked feature and you can show an upsell in its place, but taking payment and moving the customer to a higher plan is the billing product's job, not the flag's. | That division is the whole mental model. The flag guarantees a fast, consistent access decision from the plan you pass. Whether a subscription is current, how much of a quota is left, and how a customer upgrades are questions your billing system answers. Enforcing access by [segment targeting](/glossary/segment-targeting/) on plan is the same rule machinery a [targeted rollout](/glossary/targeted-rollout/) uses, pointed at tiers instead of a cohort. ## How to gate a feature by plan tier Six steps. The first is code; the rest are configuration you change without shipping. | Step | What you do | Detail | | --- | --- | --- | | 1 | Create the gate | Add a boolean flag for the premium feature with its off variation as the fallthrough, so a customer sees the feature only when a rule grants it. | | 2 | Pass the plan in context | Include the customer's plan alongside their user and organization id on every evaluation. The flag can only gate on an attribute your app actually sends. | | 3 | Add a tier rule | Configure a rule that serves the on variation when plan is in your paid tiers, for example pro or enterprise, and leave the fallthrough off for free accounts. | | 4 | Reuse a segment | Define a "Paid plans" segment once and reference it from every gated flag. Change the definition in one place and it updates across all of them. | | 5 | Override per customer | Target a specific organization id above the tier rule to grant early access, run a trial of a premium feature, or honor a bespoke contract. | | 6 | Change access live | Move a feature between tiers in the dashboard when packaging changes. Every connected SDK picks up the new rule over the stream within a second or two, with no redeploy. | Steps three and four are ordinary [targeting rules](/docs/guides/targeting-rules/) and [segments](/docs/guides/segments/) , the same building blocks a beta program or a regional launch uses. Point them at plan and they become entitlement gates. Reorder the rules so a per-customer override sits above the tier rule, since the first match wins. ## How Featureflip handles the gating side The rule and evaluation mechanics that turn a plan attribute into a fast, consistent access decision, so the only thing your billing system owns is the plan itself. - **Attribute targeting on plan.** A rule serves the on variation when plan is in your paid tiers. Rules run top to bottom and the first match wins, so a specific customer override can sit above the broad tier rule. See the [targeting docs](/docs/concepts/targeting/) for operators and ordering. - **Reusable segments.** Define a "Paid plans" or "Enterprise" segment once and reference it from every gated flag. Update the definition in one place and it propagates to all of them, so renaming or adding a tier is a single edit. See the [segments guide](/docs/guides/segments/) . - **Different limits per tier.** A multivariate flag serves a different value per plan, so one flag can return a seat or project limit that varies by tier. Your app reads it with a typed evaluation and enforces the number, with the mapping held in the flag rather than in code. - **Per-customer overrides.** Target a single organization or user id to grant a feature ahead of its tier, run a trial, or honor a contract, independent of the plan rule everyone else falls through. - **Sub-millisecond local evaluation.** The gate runs against an in-memory copy of the config, so an entitlement check on the hot path adds no network hop. The config stays local even if the control plane has a blip. - **Real-time changes.** Move a feature between tiers in the dashboard and every connected SDK picks up the new rule over a Server-Sent Events stream within a second or two, fleet-wide, with no redeploy. ## What it looks like in your app The application passes the plan in the evaluation context and asks the flag whether the feature is available. The last argument is the fallback, returned if the SDK cannot reach Featureflip, so the locked state doubles as the safe default: ```js const context = { user_id: 'user-456', org_id: 'org-123', plan: 'pro', // from your billing system }; // 'false' is the safe fallback: served when the plan is not // entitled, or when the SDK can't reach Featureflip. if (client.boolVariation('advanced-analytics', context, false)) { return renderAdvancedAnalytics(context); } return renderUpgradePrompt(context); ``` A tiered limit works the same way. A multivariate flag returns a different value per plan, so one call gives you the seat or project cap for this customer's tier: ```js // A different limit per tier: 3 on free, 25 on pro, // unlimited on enterprise. '3' is the offline fallback. const seatLimit = client.intVariation('seat-limit', context, 3); ``` The tier rules live entirely in the dashboard. Move `advanced-analytics` from enterprise into pro, or raise the `seat-limit` for a tier, and every SDK reflects it on its next evaluation without a rebuild. Because the fallback is the locked value, an outage fails safe to the free experience rather than handing out a paid feature. The same surface works in every language the platform supports, from Python and Go to C#, Java, and Node. Pick a quickstart from the [SDK overview](/docs/sdks/overview/) . ## Entitlement gating vs entitlement management A flag covers the gating side. A billing or entitlements platform covers the management side. Knowing which one you need keeps you from asking a flag to do a billing system's job. | | Entitlement gating (a flag) | Entitlement management (billing) | | --- | --- | --- | | Question it answers | Can this customer use this feature? | What is this customer entitled to, and have they hit their limit? | | What it owns | The access decision, per tier and per customer | Plan state, usage metering, quotas, billing sync | | Source of truth | Reads the plan you pass in | Owns the subscription and usage record | | How access changes | Flip a rule in the dashboard, no deploy | Plan changes flow from billing events | | Right tool | A feature flag | A billing or entitlements platform | They compose. The flag is the enforcement point inside your application, and it reads the plan your billing system owns. If all you need is to switch features on by tier, a flag is the whole answer. If you also need metering, quotas, and a checkout flow, pair the flag with a platform that owns that state and let the flag do the gating in front of it. ## Common mistakes to avoid The patterns that turn a clean entitlement gate into a support ticket. Most of them come from asking the flag to own something billing should. ### Treating the flag as the billing source of truth A flag enforces access; it does not know that a card was declined or a subscription lapsed. If the flag holds a stale copy of the plan, a downgraded customer keeps a paid feature. Keep billing authoritative and pass the current plan on every evaluation, so the gate always reflects the live subscription rather than a snapshot. ### Hard-coding the tier list in application code Writing if plan === 'pro' checks scattered through the codebase pulls the packaging decision back into the deploy pipeline. Every time a feature moves tiers, someone edits and ships code. Put the tier-to-feature mapping in targeting rules or a segment instead, so re-packaging is a dashboard change and the application code just asks whether the feature is on. ### Forgetting the safe default The fallback passed to the evaluation is what the SDK returns if it cannot reach Featureflip. For an entitlement that value should be the locked state, so an outage never hands a paid feature to a free account. Default to off and let a matched rule turn the feature on, never the other way around. ### Trying to meter usage with a flag A flag returns a value; it does not keep a running count. Enforcing 'up to 100 API calls a month' by toggling a flag when the limit is hit means something else is doing the counting anyway. Leave quotas and metering to your usage system and let the flag gate the on-or-off availability of the feature. ### Duplicating the same tier rule across flags Copying plan in [pro, enterprise] onto forty flags means forty edits the day you rename a tier or add one. Define the tier once as a segment and reference it everywhere. One update to the segment definition propagates to every flag that uses it, which is the whole reason segments exist. ## When a flag-driven entitlement is the wrong tool A flag makes gating easy, but it is not a billing system. A few cases call for something more: - **You need usage metering and hard quotas.** Counting API calls or events and enforcing a monthly limit is a metering job. A flag gates the on-or-off availability of a feature; it does not keep a running total. Meter in your usage system and let the flag gate the feature around it. - **Entitlements must sync from the billing lifecycle.** Trials expiring, dunning, proration, and mid-cycle upgrades are subscription events. A dedicated billing or entitlements platform models them; the flag only reads the plan those systems produce. Keep that state authoritative and pass the result in. - **You are building a self-serve paywall with checkout.** A flag can gate the locked feature and you can render an upsell in its place, but taking payment and moving the customer to a new plan belongs to a billing or paywall product. - **Entitlements are contractual and per-customer at scale.** Hundreds of bespoke, negotiated entitlements with complex rules are better modeled in an entitlements service. A flag still enforces the final gate in your app, reading the entitlement that service resolves. ## Frequently asked questions Can I use feature flags for entitlements? Yes. A targeting rule keyed on the plan attribute you pass at evaluation is a working entitlement gate: it serves the paid variation to customers on your paid tiers and the off variation to everyone else. You can layer a per-customer override on top to grant early access, and you can reference a reusable segment so the same tier definition drives every gated flag. The flag owns the access decision; your billing system stays the source of truth for which plan each customer is on. What is the difference between feature flags and an entitlements platform? A feature flag makes the access decision: given the plan you pass, does this customer see this feature. An entitlements platform manages the state behind that decision, syncing plans from billing events, metering usage against quotas, and driving upgrade and checkout flows. The two compose. The flag is the enforcement point in your application, and it reads the plan your billing or entitlements system owns. Many teams need only the gating side, and a flag covers that completely. How do I gate a feature by plan tier? Pass the customer's plan in the evaluation context, then add a targeting rule that serves the on variation when plan is in your paid tiers and leaves the fallthrough off. For example, a rule of plan in pro, enterprise turns a premium feature on for those customers and off for free accounts. To reuse the same tier logic across many features, define a segment for your paid plans once and reference it from each gated flag rather than recreating the rule. Can I set different limits per plan, like seats or projects? Yes. A multivariate flag serves a different value per tier, so one flag can return a seat or project limit that varies by plan: three for free, twenty-five for pro, unlimited for enterprise. Your application reads the number with a typed evaluation such as intVariation and enforces it. The tier-to-limit mapping lives in the flag's rules, so raising a limit or re-packaging a tier is a dashboard change rather than a deploy. Does Featureflip handle billing or usage metering? No, and that is deliberate. Featureflip reads the plan you pass and makes the access decision in sub-millisecond local evaluation. It does not manage subscriptions, count usage, or enforce quotas. Those stay in your billing and usage systems, which remain the source of truth. Pass the current plan on every evaluation and the gate always reflects the live subscription. If you need metering, quotas, or a checkout flow, pair the flag with a billing or entitlements platform that owns that state. Can I grant one customer early access to a paid feature? Yes. Add a rule that targets a specific organization or user id above the tier rule, so it matches first and serves the on variation regardless of plan. That gives a single account early access, a trial of a premium feature, or a contractual exception without touching the tier rule everyone else falls through. Removing the override later is a dashboard change, and every SDK picks it up over the stream within a second or two. ## Gate your next paid feature behind a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call: create a flag and add a tier rule. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### User targeting & segments (docs) Attributes, operators, and rule ordering: the mechanism an entitlement gate is built on.](/docs/concepts/targeting/) [### Using segments (guide) Define your paid tiers once and reference the segment from every gated flag.](/docs/guides/segments/) [### Progressive rollouts Stack a percentage rollout under a tier rule to release a premium feature to paid users in steps.](/use-cases/progressive-rollouts/) [### Segment targeting The glossary definition: grouping users by attributes to serve them a specific variation.](/glossary/segment-targeting/) --- ## Source: https://featureflip.io/use-cases/feature-flags-for-ai/ # Feature Flags for AI and LLM Apps [Home](/) / [Use Cases](/use-cases/) / Feature Flags for AI # Feature flags for AI and LLM apps Put model choice, the system prompt, and generation settings behind a flag. Swap models with no deploy, migrate gradually, route per tenant, and kill a bad model in seconds. The flag owns the config and routing; your eval and observability tools own whether the output is good. Last updated: July 8, 2026 **An AI feature carries a lot of config: which model to call, the system prompt, the temperature, the token limit, and which customers get which. A feature flag is a clean place to keep that config and change it live.** You ship one JSON flag holding the model settings, read it on the request path, and change the model, the prompt, or who gets them from the dashboard rather than a deploy. A percentage rollout migrates a new model in gradually and keeps each user on one model, a targeting rule routes specific tenants, and because the SDK serves a cached copy of the config, the model kill switch keeps working even if the control plane has a blip. What a flag does not do is judge whether the new model is better or track its cost. That stays with your eval and observability stack. The flag owns the config and routing decision; those tools own the quality and cost. For the mechanics, see how [rollout strategies](/docs/concepts/rollout-strategies/) and [targeting rules](/docs/concepts/targeting/) work. [Get started free](https://app.featureflip.io/signup) [Read the rollout docs](/docs/concepts/rollout-strategies/) ## Why put model choice behind a flag? Four reasons teams reach for a flag when a model, a prompt, or a provider is likely to change. - 1 ### Swap models without a deploy The model name and its settings live in config, not in a shipped constant. Move to a new model, change providers, or edit the prompt in the dashboard, and every instance reads the new value on its next evaluation. The code that builds the request never changes. - 2 ### Migrate gradually, keep it sticky Move from one model to another with a percentage rollout instead of a hard cutover. Sticky bucketing keeps each user on a single model as you ramp, so a conversation does not switch models between turns while you watch evals and cost climb the steps. - 3 ### Kill a bad model in seconds A new model that hallucinates, slows down, or runs up cost is a config change away from gone. Flip the flag or ramp the rollout to zero and every SDK reverts to the known-good model within a second or two, fleet-wide, with no rebuild and no scramble to revert a deploy. - 4 ### Route per tenant or cohort Pin an enterprise customer to a specific model, give one cohort a different prompt, or hold a tenant on the old model while everyone else migrates. A targeting rule handles it, so per-customer routing is configuration rather than a fork in your code. ## What an AI feature needs, and which half a flag covers An AI feature has a config side and a quality side. Feature flags own the config side completely. The quality side stays with the eval and observability tools you run around your models. | Part | Where it comes from | What it covers | | --- | --- | --- | | Which model to call | The feature flag | A JSON flag holds the model name, so switching from one model to another, or rolling a new one out gradually, is a config change in the dashboard rather than a code deploy. | | The prompt and generation settings | The feature flag | The system prompt, temperature, and max_tokens ride in the same JSON flag value, so you tune them and ship the change to every instance without touching application code. | | Who gets which model | The feature flag | A targeting rule routes a specific tenant, plan, or cohort to a particular model or prompt, and a percentage rollout moves everyone else over in steps. Both are dashboard settings. | | Falling back when the model fails | Your app, with the flag | Your code catches a model error and retries with the flag's fallback config, and the SDK already serves a cached copy of that config if it cannot reach Featureflip. The flag supplies the safe default; your code decides when to use it. | | Whether the new model is better | Your eval stack | Scoring output quality, catching regressions, and comparing models across a test set is the job of an eval or LLM-observability tool. The flag ships the model; it does not grade the responses. | | Token cost and latency | Your observability stack | Tracking spend per model and response latency belongs to your metrics and tracing tools. The flag can move traffic between models; your observability tells you what that move cost. | That division is the whole mental model. Holding a model name, prompt, and settings in a flag is a form of [remote config](/glossary/remote-config/) , changed from a dashboard rather than a deploy, and one JSON flag can carry several values at once the way a [multivariate](/glossary/multivariate-testing/) flag serves more than on and off. The flag guarantees a fast, consistent answer to which model and prompt this request uses. Whether the output is good, and what it cost, are questions your eval and observability tools answer. ## How to put a model behind a flag Six steps. The first four are the setup; the last two are the rollout and routing you change without shipping. | Step | What you do | Detail | | --- | --- | --- | | 1 | Create the model-config flag | Add a JSON flag whose value holds the model name, system prompt, temperature, and token limit. One flag carries the whole configuration for the feature. | | 2 | Set the safe default | The fallback you pass to the evaluation is a known-good, reliable model config. It is what the SDK serves if it cannot reach Featureflip, so an outage falls back to a model you trust rather than an experimental one. | | 3 | Read it on the request path | Call jsonVariation with the user context right before you build the model request. Evaluation is in-memory and sub-millisecond, so reading the config adds no network hop ahead of the model call. | | 4 | Add the fallback path | Wrap the model call so that if it errors, times out, or returns something unusable, your code retries with the fallback config. Pair that with the flag as a manual kill switch you can flip from the dashboard. | | 5 | Migrate a new model gradually | Stack a percentage rollout to move traffic from the current model to a new one in steps, 10 then 50 then 100. Sticky bucketing keeps each user on one model, so a single conversation does not switch models between turns. | | 6 | Route specific tenants | Add a targeting rule above the rollout to pin an enterprise customer or a cohort to a particular model or prompt, independent of the gradual migration everyone else is on. | Steps five and six are ordinary [targeting rules](/docs/guides/targeting-rules/) and a percentage rollout, the same building blocks a progressive release uses, pointed at model choice instead of a UI feature. Order the rules so a named-tenant override sits above the broad rollout, since the first match wins. ## How Featureflip handles the config side The evaluation and rule mechanics that turn a model config into a fast, consistent decision, so the only thing your other tools own is the quality and the cost. - **JSON flag values for model config.** A single `jsonVariation` flag carries the model name, system prompt, temperature, and token limit, so the whole configuration changes together in one place. See the [SDK overview](/docs/sdks/overview/) for the variation methods. - **Per-tenant and per-cohort routing.** A targeting rule serves a specific model or prompt to a plan, tenant, or segment. Rules run top to bottom and the first match wins, so an override sits above the broad rule. See the [targeting docs](/docs/concepts/targeting/) for operators and ordering. - **Sticky gradual migration.** A percentage rollout moves traffic to a new model in steps, and deterministic bucketing keeps each user on one model as you ramp, across every replica. See [rollout strategies](/docs/concepts/rollout-strategies/) . - **Sub-millisecond local evaluation.** The config read runs against an in-memory copy, so choosing the model adds no network hop before the model call, where every added millisecond of latency is felt. - **Fail-safe cached config.** Reads are served from a local cache, so the model kill switch and the current config keep working even if the control plane is briefly unreachable. The default you pass is the ultimate backstop. - **Real-time changes.** Change the model, edit the prompt, or ramp the rollout in the dashboard and every connected SDK picks it up over a Server-Sent Events stream within a second or two, fleet-wide, with no redeploy. ## What it looks like in your app The application reads the model config from the flag, passes the user context, and uses whatever the flag returns to build the request. The last argument is the fallback, a known-good config returned if the SDK cannot reach Featureflip, so the safe model doubles as the outage default: ```js const context = { user_id: 'user-456', tenant_id: 'org-123', plan: user.plan, }; // The fallback is a known-good model config: served if the SDK // can't reach Featureflip, so an outage never breaks generation. const modelConfig = client.jsonVariation('llm-model-config', context, { model: 'gpt-4o-mini', system_prompt: 'You are a helpful support assistant.', temperature: 0.7, max_tokens: 1024, }); const reply = await llm.chat({ model: modelConfig.model, temperature: modelConfig.temperature, max_tokens: modelConfig.max_tokens, messages: [ { role: 'system', content: modelConfig.system_prompt }, { role: 'user', content: userMessage }, ], }); ``` To make the model a kill switch, wrap the call so a failure falls back to the safe config, and flip the flag from the dashboard the moment a model misbehaves. Both paths return generation the user can trust: ```js // Flip 'llm-model-config' in the dashboard to swap the whole fleet // to a known-good model in seconds, with no redeploy. In code, a // failed call also falls back to the safe default config. try { return await callModel(modelConfig, userMessage); } catch (err) { const safe = { model: 'gpt-4o-mini', system_prompt: 'You are a helpful support assistant.', temperature: 0.3, max_tokens: 1024, }; return await callModel(safe, userMessage); } ``` The rollout and routing live entirely in the dashboard. Ramp `llm-model-config` from the current model to a new one, or pin a tenant to a specific model with a targeting rule, and every SDK reflects it on its next evaluation without a rebuild. Because the fallback is a model you trust, an outage fails safe to known-good generation rather than an experimental model. The same `jsonVariation` call works in every language the platform supports, from Python and Go to C#, Java, and Node. For a step-by-step Python walkthrough, see [rolling out a new LLM model behind a flag](/blog/feature-flags-for-ai-apps/) , or pick a quickstart from the [SDK overview](/docs/sdks/overview/) . ## Model config vs model quality A flag covers the config side. An eval or observability tool covers the quality side. Knowing which one you need keeps you from asking a flag to do an eval platform's job. | | Model config (a flag) | Model quality (eval tools) | | --- | --- | --- | | Question it answers | Which model, prompt, and settings does this request use, and who gets them? | Is the output any good, and what does it cost to produce? | | What it owns | The config values and the routing and rollout decision | Eval scores, output quality, token spend, and latency | | Source of truth | Reads the model config you set in the dashboard | Owns the metrics, traces, and eval results | | How it changes | Flip a value or ramp a rollout in the dashboard | Numbers flow in from your eval and observability tooling | | Right tool | A feature flag | An eval, tracing, or LLM-observability tool | They compose. The flag is the switch that decides which model and prompt run inside your application, and the eval tool owns the scores that tell you whether the change helped. If all you need is to change models, route per tenant, and be able to pull a bad one, a flag is the whole answer. If you also need to measure quality and cost, pair the flag with an eval or observability tool and let the flag deliver the model in front of it. ## Common mistakes to avoid The patterns that turn a clean setup into a mess. Most of them come from asking the flag to own something an eval, prompt, or secrets tool should. ### Treating the flag as a prompt studio A flag holds the config that is live right now and switches it. It is not a place to author, version, and diff every prompt you have ever written. Keep prompt history in your repo or a dedicated prompt tool, and let the flag pick which version is serving. ### Recomputing the model on every request If the model is chosen fresh on each call without sticky bucketing, a user can jump between models mid-conversation, which makes the experience inconsistent and the results hard to compare. Bucket on the user or conversation id so a user stays on one model until you change the rule. ### Making the experimental model the default The fallback config passed to the evaluation is what the SDK serves during an outage. If that default is the new, unproven model, a control-plane blip sends every request to it. Default to the model you trust and let a matched rule opt users into the new one. ### Expecting the flag to tell you the model is better A flag ships a model; it does not measure the output. Rolling a new model to 100% because it did not error is not the same as knowing it is better. Drive the migration with an eval harness or observability, and use the flag to act on what they show. ### Putting API keys in the flag value Model names, prompts, and numeric settings belong in the flag. Provider API keys and other secrets do not, especially since client SDKs evaluate config in the browser. Keep credentials in your secrets manager and keep only the model configuration in the flag. ## When a flag is the wrong tool A flag makes model choice and routing easy, but it is not an AI platform. A few needs call for something more: - **You need to author, version, and compare prompts.** Treating a prompt as a first-class artifact with history and diffs is a prompt-management tool or plain version control. The flag selects which prompt is live; it is not the editor or the change log. - **You need to measure output quality or run evals.** Scoring responses, catching regressions, and comparing models across a test set is an eval or observability platform's job. The flag delivers the model; it does not judge it. - **You want a statistically valid comparison of two models.** If the goal is to prove one model beats another with significance rather than to migrate safely, that is an experiment. See [A/B testing with feature flags](/use-cases/a-b-testing/) , where the flag delivers a sticky split and your analytics call the winner. - **You need a model gateway or key management.** Routing around provider outages, enforcing quotas, and holding API keys are gateway and secrets-manager concerns. A flag chooses the model; it does not broker the connection or store the credentials. ## Frequently asked questions Can I use feature flags for AI and LLM apps? Yes. The model name, system prompt, temperature, and token limit live in a single JSON flag, so you change the model or tune the prompt from the dashboard without a deploy. A targeting rule routes specific tenants to a specific model, a percentage rollout migrates a new model in gradually, and the fallback config acts as a fail-safe kill switch. The flag owns the configuration and the routing decision; your eval and observability tools own whether the output is good and what it costs. How do I build an LLM model kill switch? Put the model config behind a flag and give the evaluation a known-good fallback. When a model starts erroring, hallucinating, or running slow or expensive, change the flag value in the dashboard, or ramp its rollout back to zero, and every connected SDK picks up the change over the stream within a second or two, fleet-wide, with no redeploy. Because reads are served from a local cache, the switch keeps working even if the control plane is briefly unreachable, and the fallback config you passed is the ultimate backstop. How do I roll out a new model gradually, like gpt-4 to claude? Stack a percentage rollout on the model-config flag to move traffic from the current model to the new one in steps, for example 10 then 50 then 100 percent. Sticky bucketing keeps each user on one model as you raise the number, so a single conversation does not switch models between turns, and the cohort holds together across every replica. Watch your evals and cost between steps, and raise the percentage only when the signal looks healthy. Rolling back is the same in reverse, and it takes effect in seconds. Can I use a different model or prompt per customer? Yes. Add a targeting rule on an attribute you pass at evaluation, such as the tenant id or plan, and serve that segment a specific model or prompt. Rules run top to bottom and the first match wins, so an override for a named enterprise customer can sit above the broad rule everyone else falls through. Pinning a tenant to a model, or moving them off it, is a dashboard change that every SDK reflects on its next evaluation, with no code change and no deploy. Should I store the prompt and temperature in a feature flag? Store the live selection there. One JSON flag can hold the model name, system prompt, temperature, and max_tokens as a single value, so the whole configuration changes together and ships to every instance at once. Keep the authoring and version history of your prompts in your repo or a dedicated prompt tool, and keep provider API keys in your secrets manager rather than the flag. The flag is where you choose which configuration is live, not where you archive every version or store secrets. What is the difference between a feature flag and an AI eval tool? A feature flag decides which model, prompt, and settings a request uses and who gets them. An eval or observability tool measures the other half: whether the responses are good, whether a new model regressed, and what each call costs in tokens and latency. The two compose. The flag delivers and routes the model inside your application, and the eval tool tells you whether the change was an improvement. Many AI features need the flag from day one; add the eval tooling when you want the migration driven by data rather than by whether the model threw an error. ## Put your AI models behind a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call: create a JSON flag and read your model config from it. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Feature flag kill switch The pattern behind the model kill switch: disable a code path in production in seconds with a fail-safe fallback.](/use-cases/kill-switch/) [### Progressive rollouts The percentage-rollout mechanism that migrates a new model in steps while keeping each user on one model.](/use-cases/progressive-rollouts/) [### User targeting & segments (docs) Attributes, operators, and rule ordering: how per-tenant model and prompt routing is built.](/docs/concepts/targeting/) [### Remote config The glossary definition: changing app configuration like a model or prompt from a dashboard without a deploy.](/glossary/remote-config/) --- ## Source: https://featureflip.io/use-cases/kill-switch/ # Feature Flag Kill Switch: Disable in Seconds [Home](/) / [Use Cases](/use-cases/) / Kill Switch # Feature flag kill switches Wrap a risky feature in a flag. When it breaks, flip it off in seconds: no redeploy, no full rollback, just the safe fallback you chose. Last updated: September 22, 2026 **A feature flag kill switch wraps a risky or expensive code path in a flag so you can disable it in production the instant it misbehaves.** When something goes wrong, you set the flag to off in a dashboard. Every connected SDK serves the safe fallback on its next evaluation, within a second or two, with no redeploy, rebuild, or rollback. The code stays in the binary; no traffic runs it. Because the flag carries a default value, the same switch protects you against an unexpected outage as well as a deliberate one: if the SDK cannot reach the service, it returns the fallback you chose. Scoped to one feature, cutting it does not revert the rest of the release, which is what makes it the fastest lever you have during an incident. For the bare definition, see [what a kill switch is](/glossary/kill-switch/) . [Get started free](https://app.featureflip.io/signup) [Read the feature flag docs](/docs/concepts/feature-flags/) ## Why put a kill switch on a feature? Four reasons teams wrap risky behavior in a flag before it ever ships to everyone. - 1 ### Mitigate in seconds, not a deploy cycle A redeploy during an incident means a rebuild, a pipeline run, and a fleet rollout while the error rate keeps climbing. Flipping a flag is a single configuration change that takes effect on the next evaluation. The mitigation lands in seconds, so the clock stops before you have even found the root cause. - 2 ### Scoped to one feature A rollback reverts everything in the release, including the unrelated fixes that shipped alongside the broken feature. A kill switch disables exactly one code path and leaves the rest of the deploy untouched. You stop the one thing that is on fire without un-shipping four things that are fine. - 3 ### Decouple incident response from the pipeline When the fix is a flag, the person mitigating does not need deploy access, a green CI run, or a release window. The on-call engineer flips the switch from the dashboard at 3am and triages afterwards. The deploy pipeline stops being on the critical path of every incident. - 4 ### Pre-wired safety for risky paths The switch only helps if it exists before the incident. Wrapping a third-party call or a heavy feature in a flag at build time means the lever is already in place when the dependency degrades. You decide the safe fallback once, calmly, instead of writing it under pressure during an outage. ## What to put behind a kill switch Not every line needs a switch. The candidates are the code paths most likely to fail or get expensive independently of the rest of your app. | What to wrap | Why it earns a switch | What the fallback does | | --- | --- | --- | | A flaky third-party dependency | A payment provider, search index, or recommendations API can slow down or fail independently of your own code. | Skip the call and serve a cached or degraded result instead of hanging the request behind a dead upstream. | | A resource-heavy feature | A new query, an expensive render, or a fan-out job can spike database or CPU load only once real traffic hits it. | Drop back to the cheaper code path and shed the load while you tune or reshape the feature. | | A brand-new launch | A feature carries the most unknown risk in the first hours after it ships, before production has stress-tested it. | Serve the previous, known-good behavior the moment a metric turns red. | | A quota- or cost-bound integration | An LLM call, an SMS gateway, or a metered API can burn money or hit a hard limit if usage runs away. | Disable the calls and queue, drop, or stub the work until the spend is back under control. | A useful test: if this code path failed in production, would you want a way to disable it without shipping a new build? If yes, it earns a flag. An [AI model behind a flag](/use-cases/feature-flags-for-ai/) is a common example: a config change swaps a misbehaving model for a known-good one in seconds. The same flag you ramp up with a [progressive rollout](/use-cases/progressive-rollouts/) doubles as its kill switch once the feature is live. ## How Featureflip handles kill switches What makes the off-switch fast, safe, and dependable at the moment you actually need it. - **Server-Sent Events push.** A flag change reaches every connected SDK within a second or two, fleet-wide, with no redeploy: Featureflip pushes it over an open [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) connection the moment you flip the switch. This is the propagation guarantee that turns "off" into a real-time operation rather than a poll-interval wait. See the [streaming API](/docs/api/streaming/) for how the SSE channel works. - **Sub-millisecond local evaluation.** The kill-switch check adds no measurable latency to the hot path: the SDK evaluates from an in-memory copy of the config, so the switch you depend on during an incident never adds a network hop to every request. The config stays local even if the control plane has a blip. - **Fail-safe defaults.** Every evaluate call takes a fallback value, returned if the flag is off or the SDK cannot reach Featureflip. A deliberate kill and an unexpected outage both resolve to the safe path you picked, so the switch protects you in either failure mode. - **Environment-scoped.** Each environment holds its own flag state, so you can cut a feature in production while it keeps running in staging. See [environments](/docs/concepts/environments/) for how configuration is separated. - **Composable with rollouts.** The kill switch and the [percentage rollout](/docs/concepts/rollout-strategies/) are two ends of the same dial: ramp a feature up gradually, then cut it to zero instantly if it misbehaves. One flag, both behaviors, no extra wiring. - **Audit trail.** On the Business plan, the audit log records who flipped the switch and when, so the post-incident review has the "who turned it off, and at what time" answer without guesswork. This toggle is the kill switch. Flip it off on the Production tab and every connected SDK has the change within a second or two, while Development carries on untouched. ## What it looks like in your app A kill switch is the ordinary flag-evaluation pattern with one rule: the third argument, the fallback, must be the *safe* value. That fallback is what the SDK returns when the flag is off and when it cannot reach Featureflip, so it has to represent the feature being disabled. ```js // 'false' is the fallback: returned if the flag is off OR if the // SDK can't reach Featureflip. Pick the safe value, not the risky one. if (client.evaluate('recommendations-enabled', user, false)) { return renderRecommendations(user); // the expendable feature } return renderWithoutRecommendations(user); // the safe fallback ``` Set the flag to off in the dashboard and every SDK returns `false` on its next evaluation: the recommendations block stops rendering everywhere within seconds. Because the default is `false`, a control-plane outage also drops to the safe path rather than leaving the risky feature running. The application code never changes between an incident and a normal day. The same surface works in every language the platform supports, from Python and Go to C#, Java, and Node. Pick a quickstart from the [SDK overview](/docs/sdks/overview/) . ## Kill switch vs rollback vs circuit breaker Three tools that all reduce blast radius, but they are not interchangeable. The differences are trigger, scope, and speed. | | Kill switch | Rollback | Circuit breaker | | --- | --- | --- | --- | | Trigger | A human, deliberately | A human, deliberately | Automatic, on error rate or latency | | Scope | One feature | The whole release | One dependency call | | Speed | Next evaluation (seconds) | Rebuild and redeploy (minutes) | Immediate, per request | | What happens to the code | Stays deployed, dormant | Removed from production | Stays; calls short-circuit | | Best for | Disabling a known-risky feature on demand | Reverting a bad deploy wholesale | Shedding load from a failing dependency without a human | They compose well. A [circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html) handles the automatic, per-request case (a dependency degrades, calls short-circuit on their own); a kill switch handles the deliberate case (you have judged a feature risky and want it off); a rollback is the heavier lever for when the whole release is bad. Reaching for the right one is half of a calm incident. ## Common mistakes to avoid The patterns that turn a kill switch into a switch that does nothing, or makes things worse, when you finally pull it. ### Failing open instead of failing safe The fallback value decides what happens when evaluation can't run. If you default to the risky path, an outage that stops evaluation leaves the dangerous code executing, the exact opposite of what a kill switch is for. Default every kill switch to the safe value so a deliberate flip and an unexpected blip both land on the same safe behavior. ### Never exercising the off path The fallback branch rots when nobody runs it. The day you finally flip the switch, you discover the legacy path was deleted, throws, or renders broken. Exercise the off path in CI or staging on a schedule so it still works when production needs it. ### A kill switch that depends on what it kills If the flag configuration is fetched through the same gateway, database, or service that is failing, the switch dies with it. Keep the flag-evaluation path independent of the dependency it protects. Featureflip evaluates from an in-memory copy of the config, so the SDK can still read the flag even when the protected dependency is down. ### No alert to tell you to flip it A kill switch is a manual lever. Without monitoring on the wrapped code path, nobody knows the feature is misbehaving until a customer reports it. Pair every kill switch with an alert on the metric that would justify pulling it. ### Flipping the wrong environment Each environment holds its own flag state. Cutting the switch in staging does nothing for the production incident; cutting it in production during a test takes down live traffic. Confirm the environment before you flip, every time. ### Treating an ops toggle like a release toggle A rollout flag is short-lived: you delete it once the feature hits 100%. A kill switch is legitimately long-lived: it earns its keep precisely when something breaks, which might be months later. Keep it, but document it clearly so a later cleanup pass does not mistake it for flag debt. ## When a kill switch is the wrong tool A kill switch only works when there is a safe path to fall back to. A few situations do not offer one: - **Irreversible actions.** A schema migration that has already altered the table, an email already sent, a card already charged: flipping a flag cannot un-do any of these. Reach for staged migrations and idempotent, replayable jobs instead, and put the switch in front of the action, not after it. - **Failures that need handling per request, automatically.** If the right response is "retry, then degrade the moment this dependency errors," that is a circuit breaker tripping itself, not a human flipping a switch. Use the breaker for the automatic case and keep the kill switch for the deliberate one. - **Core functionality with no safe fallback.** If a feature has no degraded mode, then "off" is just a different outage. The work is to build the fallback first; the switch is only as useful as the path it falls back to. - **Changes that must apply uniformly.** A compliance-mandated behavior or a security patch usually cannot be left disabled for some users and on for others. Ship it to everyone and monitor, rather than gating it behind a switch you are reluctant to leave off. ## Frequently asked questions What is a feature flag kill switch? A feature flag kill switch is a flag whose only job is to turn a feature off quickly. You wrap a risky or expensive code path in a flag, and when it misbehaves in production you set the flag to off in the dashboard. Every connected SDK serves the safe fallback on its next evaluation, with no deployment, rebuild, or rollback. The code stays in the binary, but no traffic runs it, so you can mitigate first and diagnose later. How quickly does a kill switch take effect? With Featureflip, typically within a second or two. The dashboard pushes the change to every connected SDK over a Server-Sent Events stream, and the next flag evaluation uses the new value fleet-wide. SDKs configured for polling pick the change up on their next interval instead. Either way it is a configuration change, not a deploy, so there is no rebuild or rollout cycle between flipping the switch and the feature going dark. How is a kill switch different from a rollback? A rollback reverts a deployment: it rebuilds and redeploys, it is coarse (it reverts everything in that release, not just the broken feature), and it is slow and risky to run mid-incident. A kill switch is scoped to a single feature and takes effect on the next flag evaluation. The new code stays deployed but dormant, so you can stop the bleeding in seconds and then triage the root cause without the deploy pipeline on the critical path. How is a kill switch different from a circuit breaker? A circuit breaker is automatic: it watches error rates or latency on a specific dependency call and trips itself, per request, without a human. A kill switch is deliberate: a person decides a feature should be off and flips it. They are complementary. Use a circuit breaker to shed load from a failing dependency the instant it degrades, and a kill switch to disable a feature you have judged risky on your own schedule. Many teams wire both around the same risky integration. What should the kill switch's default value be? The safe one. Every evaluate call takes a fallback value that the SDK returns if the flag is off or if it cannot reach Featureflip. Choose the value that represents the feature being disabled, so a deliberate kill and an unexpected outage both resolve to the safe path. Defaulting to the risky value means a network blip silently re-enables the very thing you were guarding against. Can I disable a feature in one environment without affecting others? Yes. Flag state is per environment, so you can cut a feature in production while it keeps running in development or staging, and vice versa. That separation is what lets you flip a production kill switch during an incident without disturbing the environments your team is actively building in. ## Wrap your next risky feature in a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call: sign up and put a switch on it. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Kill switch (definition) The glossary entry: the short, plain-English definition and how it differs from a rollback.](/glossary/kill-switch/) [### Progressive rollouts The other end of the dial: ramp a feature up gradually, with the same flag ready to cut it to zero.](/use-cases/progressive-rollouts/) [### What are feature flags? The concept page covering flags, variations, environments, and the evaluation model.](/docs/concepts/feature-flags/) [### Feature flag anti-patterns How teams hurt themselves with flags, including kill switches that fail open or never get tested.](/blog/feature-flag-anti-patterns/) --- ## Source: https://featureflip.io/use-cases/progressive-rollouts/ # Progressive Rollouts with Feature Flags [Home](/) / [Use Cases](/use-cases/) / Progressive Rollouts # Progressive rollouts with feature flags Ship to 1% of users first. Watch the metrics. Expand when it's safe. Roll back in seconds when it's not. Last updated: September 22, 2026 **A progressive rollout exposes a new feature to a small slice of users first** — usually 1% or 5% — then expands the audience in stages once each tier shows healthy metrics. Feature flags make the percentage a configuration change instead of a redeploy: change the number in a dashboard, the SDK receives it over a streaming connection, the new path lights up for the configured share of traffic. The same users stay in the enabled cohort as you ramp, so you are not re-randomizing the population at every step. The point is to limit blast radius, get real production signal before broad exposure, and turn rollback into a 30-second operation instead of an incident. [Get started free](https://app.featureflip.io/signup) [Read the rollout strategy docs](/docs/concepts/rollout-strategies/) ## Why ship features behind a progressive rollout? Four reasons teams reach for a percentage gate before flipping a feature to everyone. - 1 ### Limit blast radius A bug in front of 1% of users is annoying. A bug in front of 100% is an outage. Progressive rollouts cap the exposure of any single change at a level you choose, so an unfortunate regression hits a small fraction of traffic instead of the whole product. - 2 ### Real production signal Staging environments do not have your real user mix, your real data shapes, or your real concurrency. Even a careful canary deploy can miss the long-tail behavior that only shows up at production scale. A 1% rollout in production exposes the new code path to actual users without committing the entire fleet. - 3 ### Decouple deploy from release When the rollout lives in a flag, you can deploy code that nobody is running yet. The deploy becomes a low-risk event — the binary is in place, the rollout starts when you decide. Marketing or product timing stops constraining the deploy cadence, and a Friday-afternoon merge stops being a scary one. - 4 ### Rollback without a deploy Flipping a flag is faster than reverting a commit and rebuilding the fleet. During an incident, "set the percentage to 0% and watch the error rate fall" is a 30-second operation. The new code is still in the binary, but no traffic executes it. The team can then triage the root cause without the clock running. ## The standard rollout ladder A pattern that works for most product changes: five steps, each gated on the previous step looking healthy. The numbers are starting points — adjust for your traffic and risk profile. | Step | What you are doing | What to watch | | --- | --- | --- | | 1% | Smoke test in production | Crashes, error-rate spikes, anything obviously broken in the new code path. If 1% looks clean for a few hours, you have permission to grow the cohort. | | 5% | Wider real-user mix | Latency at p95 and p99, error rates on the new code path versus the control. Five percent surfaces the long-tail user states that 1% misses — odd browsers, stale clients, paused subscriptions. | | 25% | Cross-cohort signal | Conversion and business metrics that need volume to move (checkouts, sign-ups, retention proxies). At 25% you can A/B compare against the 75% control with statistical weight inside a working day. | | 50% | Halfway, hold for a beat | Anything cyclical — daily peaks, end-of-month batch jobs, scheduled cron. Half the population means you can compare like-for-like across the cohort split when a regression turns out to be load-related. | | 100% | General availability | The new path is the only path. Keep the flag in place for one full release cycle in case you need to flip it back, then archive it. Stale flags become invisible branches over time. | A common variant pairs the percentage rollout with a [targeting rule](/docs/concepts/targeting/) that serves the new path unconditionally to internal staff. Your team sees the feature at 100% from day one — dogfooding catches the obvious problems — while general users follow the ladder. This is the canary pattern composed with a percentage rollout. ## How Featureflip handles progressive rollouts The mechanics that make a rollout safe to ramp instead of a coin-flip at every step. For a closer look at the dial itself, see the [percentage rollout feature](/product/features/percentage-rollouts/) . - **Sticky bucketing via deterministic hashing.** Featureflip combines the user ID and the flag key into a hash that produces a stable bucket assignment. The same user always lands in the same bucket for the same flag, so growing the rollout adds users to the enabled cohort — it does not re-randomize the existing one. See the [rollout strategies docs](/docs/concepts/rollout-strategies/) for the algorithm. - **Sub-millisecond local evaluation.** The SDKs hold the flag config in memory and evaluate locally — no network hop per request, no shared dependency on a flag service to keep the request path alive. Adding a flag to a hot code path does not add measurable latency. - **Server-Sent Events push.** When you change a percentage in the dashboard, every connected SDK receives the new config over its open SSE connection within seconds. The next flag evaluation uses the new percentage, fleet-wide, with no redeploy. This is the propagation guarantee that makes "ramp by tenths of a percent" practical. - **Composable with targeting rules.** A percentage rollout on the fallthrough is the default, but you can stack rules above it — internal staff get unconditional access, beta users get unconditional access, everyone else hits the percentage. See [setting up targeting rules](/docs/guides/targeting-rules/) for the wiring. - **Built-in kill switch.** Disable a flag entirely from the dashboard and every SDK serves the fallback variation within seconds. This is the incident-mitigation lever — separate from the percentage ramp, available at any step. See [using a flag as a kill switch](/use-cases/kill-switch/) for the full pattern. At 40%, one rung up the ladder above. The preview shows who gets what before you save. ## What it looks like in your app The application code does not know it is in a rollout. It asks the SDK for the value of a flag, gets `true` or `false`, and branches accordingly: ```js if (client.evaluate('checkout-v2', user, false)) { return renderNewCheckout(user); } return renderLegacyCheckout(user); ``` The percentage lives entirely in the dashboard. At 0% every request gets `false`. At 5%, a stable 5% of authenticated users get `true`. The application code is identical at every step of the ladder. The same pattern works in every language the platform supports — pick a quickstart from the [SDK overview](/docs/sdks/overview/) and the surface is consistent. ## Common pitfalls to avoid Patterns that show up in incident retros across most teams that run rollouts. ### Forgetting to pass user_id Percentage rollouts use a deterministic hash of the user identifier plus the flag key. If the SDK gets called without a stable user_id, the hash falls through to the default branch every time — so every request from that caller sees the control variation. Server-side jobs and unauthenticated traffic are the usual offenders. Either pass a stable session ID or accept that anonymous traffic gets the fallthrough value. ### Re-randomizing mid-rollout If you change the targeting key between steps — say, switching from user_id to organization_id when you bump to 25% — every user gets rebucketed. People who saw the new feature at 5% suddenly do not, and vice versa. Pick one identifier per flag and keep it for the life of the rollout. ### Skipping the cleanup step 100% rollout is not the end. Once the feature is stable and the rollback window has passed, remove the flag from the code and archive it in the dashboard. Codebases that never delete flags end up with dozens of conditional branches that nobody dares to touch — every refactor turns into archaeology. ### Treating the rollout as the only safety net A progressive rollout limits blast radius. It does not detect problems for you. Alerts, dashboards, and the discipline to actually watch them during a ramp are what catch issues at 1% so they never reach 100%. The flag is the brake; the monitoring is what tells you to use it. ### Schema migrations behind a flag Feature flags are great for application code, weaker for irreversible changes. A schema migration cannot be rolled back by flipping a flag — the table is already altered. Stage migrations so the old and new shapes coexist for the duration of the rollout, then run a second migration to retire the old shape once the flag hits 100%. ## When a progressive rollout is the wrong tool Most product changes benefit from a percentage gate, but a few do not. Save yourself the ceremony when: - **The change is genuinely atomic.** An API contract version, a wire-format upgrade, or a single-tenant cutover does not split cleanly across users. Use a maintenance window or a coordinated swap instead. - **The change is compliance-driven and must apply uniformly.** A GDPR-mandated behavior change or a security patch typically cannot exclude 95% of users while you watch the other 5%. Ship to 100% and monitor. - **There is no per-user variability in what you are changing.** Backend infrastructure swaps — a Redis upgrade, a queue migration — usually do not have a per-user path. A canary deployment of the service is the better safety net. - **The change is too small to instrument.** Cosmetic copy tweaks and trivial bugfixes do not earn the overhead of a ramp. Ship them, watch the broader release metrics, move on. ## Frequently asked questions What is a progressive rollout? A progressive rollout exposes a new feature to a small percentage of users first — typically 1% or 5% — and then expands the audience in steps once each tier shows healthy metrics. Feature flags make the percentage a configuration change instead of a redeploy: you change the percentage in a dashboard, the SDKs receive the new config, and the change takes effect within seconds. The same users stay in the enabled cohort as the percentage grows, so you do not re-randomize the population at every step. What percentage should I start a rollout at? Most teams start at 1% or 5%. The right floor is whichever is high enough to produce meaningful signal in your monitoring within a working day and low enough that an outage in the new path is annoying rather than incident-worthy. For a high-traffic consumer app, 1% may be thousands of users per minute — plenty of signal. For a low-volume B2B product, 5% or even 10% may be the practical floor. How is a progressive rollout different from a canary release? The two terms overlap. A canary release typically routes a small slice of traffic to a separate deployment (a "canary" instance) running the new code; you watch the canary, then either promote the build to the fleet or kill it. A progressive rollout uses a feature flag to gate the new code path inside a single deployment — the new code is already running everywhere, but only a configured percentage of users execute it. Progressive rollouts are cheaper to set up (no separate infrastructure) and more granular (you can split inside a single service), but they require the new code to be deployable before it is exposed. Will users flip between the new and old experience as the rollout grows? No, provided you keep the targeting key stable. Featureflip's SDKs use a deterministic hash of the user identifier and the flag key. The same user always produces the same bucket assignment for the same flag, so when you expand from 5% to 25% you are adding new users — not re-randomizing the original 5%. This is called sticky bucketing, and it is what makes progressive rollouts safe to compose with experiments. Can I run a progressive rollout without redeploying? Yes — that is the entire point of using a feature flag for rollouts. Deploy the new code with the flag at 0% (off for everyone). Once the deploy is healthy, change the percentage in the Featureflip dashboard. The SDK receives the new config over an SSE connection and applies it in-process within seconds. Rolling back is just as fast: set the percentage to 0% and the new code path stops executing on the next evaluation. How quickly does a percentage change propagate? Featureflip uses Server-Sent Events to push config changes from the dashboard to every connected SDK in real time. Propagation is typically within a second or two for SDKs with an open SSE connection. SDKs configured for polling pick up changes on the next poll interval. In an incident, this is the difference between a one-minute mitigation and a redeploy cycle. ## Start your next rollout behind a flag The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) ## Related [### Rollout strategies (docs) Deterministic hashing, fallthrough behavior, and how rules combine with percentage splits.](/docs/concepts/rollout-strategies/) [### What are feature flags? The concept page covering flags, variations, environments, and the evaluation model.](/docs/concepts/feature-flags/) [### Feature flag anti-patterns The other side of the coin — common ways teams hurt themselves with flags, and how to avoid them.](/blog/feature-flag-anti-patterns/) [### Kill switches The other end of the dial — cut a feature to zero instantly when a metric turns red mid-rollout.](/use-cases/kill-switch/) --- ## Source: https://featureflip.io/glossary/ # Feature Flag Glossary [Home](/) / Glossary # Feature flag glossary Plain-English definitions of the terms that come up when teams adopt feature flags — what each one means, and how the ideas relate. [Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code. Read the definition →](/glossary/feature-flag/) [Feature toggle Feature toggle is another name for a feature flag — a runtime switch that turns functionality on or off without a code deploy. The terms are interchangeable. Read the definition →](/glossary/feature-toggle/) [Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up. Read the definition →](/glossary/percentage-rollout/) [Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required. Read the definition →](/glossary/kill-switch/) [Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule. Read the definition →](/glossary/dark-launch/) [Canary release A canary release exposes a new version to a small group of users first and watches its metrics before rolling it out to everyone — so problems surface on a fraction of traffic instead of the whole fleet. Read the definition →](/glossary/canary-release/) [Progressive delivery Progressive delivery is the practice of releasing software gradually and reversibly — using feature flags, canary releases, and percentage rollouts to expand exposure step by step while watching metrics, instead of shipping to everyone at once. Read the definition →](/glossary/progressive-delivery/) [A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess. Read the definition →](/glossary/a-b-testing/) [Experimentation Experimentation is the practice of making product decisions from controlled experiments — running A/B and multivariate tests, measuring outcomes against a hypothesis, and shipping the version the data supports rather than the one with the loudest advocate. Read the definition →](/glossary/experimentation/) [Trunk-based development Trunk-based development is a branching model where everyone commits small, frequent changes straight to a single shared branch — keeping integration continuous and avoiding the merge pain of long-lived feature branches. Read the definition →](/glossary/trunk-based-development/) [Stale flag A stale flag is a feature flag that no longer makes a live decision — it serves one variation to everyone, receives no traffic, or was never used — yet still exists as a conditional branch in code. Read the definition →](/glossary/stale-flag/) [Flag debt Flag debt is the accumulated maintenance burden of feature flags that have outlived their purpose but still sit in the codebase as conditional branches nobody has removed. Read the definition →](/glossary/flag-debt/) [Multivariate testing Multivariate testing compares more than two variations at once — often combinations of several changes — to find which performs best, extending an A/B test beyond a single either/or split. Read the definition →](/glossary/multivariate-testing/) [Segment targeting Segment targeting serves a flag variation to a named, reusable group of users defined by shared attributes — like "beta users" or "EU customers" — instead of listing individuals one by one. Read the definition →](/glossary/segment-targeting/) [Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context. Read the definition →](/glossary/flag-evaluation/) [Targeted rollout A targeted rollout releases a feature to a specific audience — defined by user attributes or a segment — rather than to a random percentage of everyone, so the right users get it first. Read the definition →](/glossary/targeted-rollout/) [Feature management Feature management is the practice, and the category of tools, for running feature flags as a system: defining, targeting, rolling out, and retiring them from one place without redeploying. Read the definition →](/glossary/feature-management/) [Remote config Remote config stores application settings on a server so you can change them without shipping a release, using the same delivery mechanism a feature flag does but for values like limits, copy, and thresholds rather than on/off decisions. Read the definition →](/glossary/remote-config/) [Variation A variation is one of the possible values a feature flag can return — true or false for a boolean flag, or one of several strings, numbers, or JSON objects for a multivariate flag. Read the definition →](/glossary/variation/) [Sticky bucketing Sticky bucketing assigns each user to a flag variation with a deterministic hash of their identifier, so the same user always lands in the same bucket and stays there even as you raise a rollout percentage. Read the definition →](/glossary/sticky-bucketing/) [Blue-green deployment A blue-green deployment runs two identical production environments — one live, one holding the new release — and switches all traffic over at once, with an instant switch back if the new version misbehaves. Read the definition →](/glossary/blue-green-deployment/) [OpenFeature OpenFeature is a CNCF open standard that defines one vendor-neutral API for evaluating feature flags, with a swappable provider behind it, so changing flag vendors becomes a configuration change instead of a rewrite. Read the definition →](/glossary/openfeature/) [Model Context Protocol (MCP) The Model Context Protocol is an open standard for giving AI assistants callable tools, and an MCP server for feature flags lets an assistant like Claude Code or Cursor create, toggle, and clean up flags without leaving the editor. Read the definition →](/glossary/model-context-protocol/) [Flags as code Flags as code is the practice of declaring feature flag configuration in version-controlled files, so flags, variations, segments, and targeting rules are reviewed in pull requests and applied by CI rather than edited in a dashboard. Read the definition →](/glossary/flags-as-code/) [Continuous delivery Continuous delivery is the practice of keeping every change in a deployable state and automating the path to production, so shipping becomes a routine decision rather than a scheduled event. Read the definition →](/glossary/continuous-delivery/) [Beta testing Beta testing puts a pre-release feature in front of a limited group of real users to gather feedback and find defects under real conditions, before it reaches everyone. Read the definition →](/glossary/beta-testing/) [Dogfooding Dogfooding is the practice of running your own unreleased features internally, so your team hits the defects before any customer does. Read the definition →](/glossary/dogfooding/) [Client-side testing Client-side testing decides the variation inside the user's browser or mobile app, using a client SDK and a client key that can read only the flags you have marked visible to the client. Read the definition →](/glossary/client-side-testing/) [Server-side testing Server-side testing decides the variation in your own backend before the response leaves it, using a server SDK and a server key that can read every flag in the project. Read the definition →](/glossary/server-side-testing/) More terms are added as the glossary grows. For deeper explanations, start with [the feature flags concept guide](/docs/concepts/feature-flags/) . ## Put the concepts to work Start on the Free plan — 10 flags, 2 environments, no credit card. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/a-b-testing/ # A/B testing — Definition [Home](/) / [Glossary](/glossary/) / A/B testing # A/B testing Also called: split testing Last updated: May 28, 2026 An **A/B test** is a controlled experiment that compares two variations — A, the control, and B, the treatment — by randomly assigning users to one or the other and measuring a target metric such as conversion or retention. Because assignment is random and the groups run concurrently, a statistically significant difference can be attributed to the change itself rather than to seasonality or audience mix. ## How feature flags run an A/B test A multivariate flag is the delivery mechanism: it serves variation A to one half of users and B to the other, assigning each user by a deterministic hash so they stay in the same group for the life of the test — the same [percentage rollout](/glossary/percentage-rollout/) bucketing, split for measurement rather than for a gradual ramp. The flag decides *who sees which variation*, and a metrics pipeline records how each group behaves so you can compare them. Where that decision happens is a separate choice: [server-side](/glossary/server-side-testing/) keeps the rule and its inputs private, while [client-side](/glossary/client-side-testing/) puts the variation in the browser before anything renders. ## A/B test vs percentage rollout They share the bucketing machinery but answer different questions. A [percentage rollout](/glossary/percentage-rollout/) asks "is this safe to expand?" and ramps one variation toward 100%. An A/B test asks "which variation is better?" and holds a stable split long enough to gather a significant result. A/B testing is the simplest form of [experimentation](/glossary/experimentation/) ; richer designs test more than two variants at once. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Experimentation Experimentation is the practice of making product decisions from controlled experiments — running A/B and multivariate tests, measuring outcomes against a hypothesis, and shipping the version the data supports rather than the one with the loudest advocate.](/glossary/experimentation/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/beta-testing/ # Beta testing — Definition [Home](/) / [Glossary](/glossary/) / Beta testing # Beta testing Also called: beta program, beta release Last updated: August 4, 2026 **Beta testing** is a release stage where a feature goes to a defined group of real users while everyone else carries on without it. You are trying to learn from usage you cannot simulate. Real users arrive with data shapes and hardware no test suite covers. With feature flags the beta cohort is a targeting rule rather than a separate build, so joining or leaving the beta is a configuration change and the code ships once. ## How a beta cohort is defined The cohort is whoever a [targeted rollout](/glossary/targeted-rollout/) rule matches. Common shapes are an opt-in attribute your app sets when someone clicks "join the beta", a plan tier, an allowlist of accounts, or a [segment](/glossary/segment-targeting/) you reuse across several flags. Membership is re-checked on every [evaluation](/glossary/flag-evaluation/) , so a user who opts out stops seeing the feature on their next request and nothing needs redeploying. The [targeting guide](/docs/concepts/targeting/) covers the rule model and the operators available. ## Beta testing and the stages either side of it [Dogfooding](/glossary/dogfooding/) comes first, using your own staff. The feedback is expert and the tolerance for breakage is high. A beta widens that to real users who agreed to be early. They bring variety your own team cannot produce. With a [canary release](/glossary/canary-release/) the question changes, because sampling ordinary traffic checks that the change is safe without asking anyone what they think of it. Most teams run all three on the same flag, widening the rule at each step. Want the full picture? Read the concept guide: [Targeting →](/docs/concepts/targeting/) ## Related terms [### Dogfooding Dogfooding is the practice of running your own unreleased features internally, so your team hits the defects before any customer does.](/glossary/dogfooding/) [### Targeted rollout A targeted rollout releases a feature to a specific audience — defined by user attributes or a segment — rather than to a random percentage of everyone, so the right users get it first.](/glossary/targeted-rollout/) [### Segment targeting Segment targeting serves a flag variation to a named, reusable group of users defined by shared attributes — like "beta users" or "EU customers" — instead of listing individuals one by one.](/glossary/segment-targeting/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/blue-green-deployment/ # Blue-green deployment — Definition [Home](/) / [Glossary](/glossary/) / Blue-green deployment # Blue-green deployment Also called: blue-green deploy, red-black deployment Last updated: July 7, 2026 A **blue-green deployment** keeps two production environments side by side. One, *blue*, serves all live traffic while the other, *green*, receives the new release and is verified in isolation. When green is ready the router flips all traffic to it in a single cutover; if a problem appears, you flip straight back to blue. The switch happens at the infrastructure layer and is all-or-nothing. ## Blue-green deployment vs feature flags They operate at different layers. Blue-green swaps the whole environment at once: every user moves to the new build together, and rollback means pointing the router back at blue. A [feature flag](/glossary/feature-flag/) works one level up, inside a single running environment, toggling an individual feature and serving it to a [percentage](/glossary/percentage-rollout/) or a targeted segment of users rather than everyone at once. Blue-green answers "which version of the whole app is live?"; a flag answers "which users see this one feature?". A [canary release](/glossary/canary-release/) sits between them, shifting a fraction of traffic to the new version first. ## Using them together The two compose cleanly, because they solve different halves of a release. Deploy the new build with blue-green while the risky feature is [dark-launched](/glossary/dark-launch/) behind a flag held at 0%, cut traffic over to green, then ramp the feature with a [percentage rollout](/glossary/percentage-rollout/) on its own schedule. That is the deploy-versus-release split: blue-green gets the code safely into production, and the flag controls when users actually see the feature. The [rollout strategies guide](/docs/concepts/rollout-strategies/) shows how the serving side is configured. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Canary release A canary release exposes a new version to a small group of users first and watches its metrics before rolling it out to everyone — so problems surface on a fraction of traffic instead of the whole fleet.](/glossary/canary-release/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/canary-release/ # Canary release — Definition [Home](/) / [Glossary](/glossary/) / Canary release # Canary release Also called: canary deployment, canary testing Last updated: July 7, 2026 A **canary release** rolls out a change to a small slice of users — the canary — before everyone else. You monitor error rates, latency, and business metrics for that group; if the canary stays healthy you widen exposure, and if it degrades you pull back having affected only a fraction of traffic. The name comes from the canary in a coal mine: an early warning that costs little to lose. ## How a canary release works with feature flags With a flag, the canary is a cohort rather than a separate pool of servers or a second environment you cut over to in a [blue-green deployment](/glossary/blue-green-deployment/) . You serve the new variation to a small share of users through a [percentage rollout](/glossary/percentage-rollout/) , or to a deliberately chosen group such as internal staff through a [targeted rollout](/glossary/targeted-rollout/) , watch the metrics for that cohort, and widen exposure when the signal is clean. Because the new code already shipped behind the flag, promoting or aborting the canary is a configuration change — no redeploy, and a [kill switch](/glossary/kill-switch/) straight back to the old path if the canary turns sick. The [rollout strategies guide](/docs/concepts/rollout-strategies/) covers how the serving is configured. ## Canary release vs progressive delivery A canary release is the *first, smallest step* of a wider rollout — the initial exposure whose job is early detection. [Progressive delivery](/glossary/progressive-delivery/) is the broader practice that contains it: start with a canary, then ramp through larger tiers (often a [beta cohort](/glossary/beta-testing/) next) to 100% as the metrics hold. Put another way, a progressive rollout usually opens with a canary, but the canary is just the first move, not the whole sequence. See [progressive delivery in depth](/blog/progressive-delivery-explained/) for how the full sequence fits together. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Progressive delivery Progressive delivery is the practice of releasing software gradually and reversibly — using feature flags, canary releases, and percentage rollouts to expand exposure step by step while watching metrics, instead of shipping to everyone at once.](/glossary/progressive-delivery/) [### Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required.](/glossary/kill-switch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/client-side-testing/ # Client-side testing — Definition [Home](/) / [Glossary](/glossary/) / Client-side testing # Client-side testing Also called: client-side experimentation Last updated: August 4, 2026 **Client-side testing** is any test or rollout whose variation is decided in the user's own browser or mobile app. A client SDK holds the flag configuration in memory and evaluates it there, so the interface can branch on the result without a round trip to your servers. Code running on someone else's device can be read by whoever owns that device, so everything the SDK receives has to be safe to expose. The key type is what enforces that. ## What a client key can see Featureflip issues two kinds of SDK key, prefixed `sdk_client_` and `sdk_server_`. A client key returns only the flags marked **client-side visible**, which makes it safe to ship inside a browser bundle. Every other flag in the project stays invisible to it, including the ones whose names alone would give away unreleased work, and because that filter is applied when the configuration is served, an unmarked flag never reaches the device in the first place. The [browser SDK reference](/docs/sdks/browser/) covers setup and the key type it expects. ## What this changes about a test Deciding on the device makes purely visual changes easy, because the variation is available before anything renders. It also rules out secrecy. A curious user can read the flag payload straight out of the bundle, so a client-side test should carry no credential and no unannounced feature name. Cost behaves differently too, since each device holds its own connection and that is the unit metered by vendors charging per monthly active user. Assignment stays stable throughout, because [bucketing](/glossary/sticky-bucketing/) hashes the user key deterministically rather than reading anything off the device. Want the full picture? Read the concept guide: [Browser SDK →](/docs/sdks/browser/) ## Related terms [### Server-side testing Server-side testing decides the variation in your own backend before the response leaves it, using a server SDK and a server key that can read every flag in the project.](/glossary/server-side-testing/) [### A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess.](/glossary/a-b-testing/) [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/continuous-delivery/ # Continuous delivery — Definition [Home](/) / [Glossary](/glossary/) / Continuous delivery # Continuous delivery Also called: CD Last updated: August 4, 2026 **Continuous delivery** (CD) is the practice of building, testing, and packaging every change so that it could go to production at any moment, with the path there automated. The pipeline runs on each commit. A person still decides when to ship, and continuous *deployment* removes even that step by pushing every passing build straight out. Both stop at the point where code lands in production, which is where [progressive delivery](/glossary/progressive-delivery/) takes over and starts managing exposure. ## Why continuous delivery needs feature flags A pipeline that ships on every commit has to cope with work that is only half done. Feature flags handle this by splitting apart the two things a release does at once. The merge and the deploy happen whenever the pipeline says so. Turning the feature on is a separate decision you make later, by hand, once the work is actually finished. Unfinished work sits in production behind a flag held off, which is a [dark launch](/glossary/dark-launch/) , so the trunk stays green and deployable and nobody has to hold a branch back to keep it that way. That is also the mechanism that makes [trunk-based development](/glossary/trunk-based-development/) workable at speed. ## Delivery, deployment, and release The three words get used interchangeably and mean different things. Delivery means the change is proven and ready to go, and deployment puts that binary onto production machines. Release is the moment a user can see it. Continuous delivery automates the first two, and a feature flag is what lets you hold the third back, so a deployment carries no user-visible risk and a release needs no deploy. Flag state is held per environment, which means the same build can run everywhere while the flag stays on in staging and off in production. The [environments guide](/docs/concepts/environments/) covers how that state is kept separate. Want the full picture? Read the concept guide: [Environments →](/docs/concepts/environments/) ## Related terms [### Trunk-based development Trunk-based development is a branching model where everyone commits small, frequent changes straight to a single shared branch — keeping integration continuous and avoiding the merge pain of long-lived feature branches.](/glossary/trunk-based-development/) [### Progressive delivery Progressive delivery is the practice of releasing software gradually and reversibly — using feature flags, canary releases, and percentage rollouts to expand exposure step by step while watching metrics, instead of shipping to everyone at once.](/glossary/progressive-delivery/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/dark-launch/ # Dark launch — Definition [Home](/) / [Glossary](/glossary/) / Dark launch # Dark launch Also called: dark launching Last updated: May 28, 2026 A **dark launch** deploys a feature into production without releasing it to users. The code is live — running, or ready to run behind a [flag](/glossary/feature-flag/) at 0% — but invisible. You validate it under real production conditions first, then make it visible when you decide. ## Two kinds of dark launch There are two common flavors. A **back-end dark launch** runs the new code path without surfacing its results — shadow traffic, dual-writes, or computing a value you discard — to test load and correctness against real data. A **hidden-feature dark launch** deploys the user-facing feature behind a flag held at off, then flips it on later. Feature flags enable both: the code ships, the exposure is a separate decision. ## Dark launch vs progressive rollout A dark launch is about *deploying without releasing* — getting code safely into production while it stays invisible. A [percentage rollout](/glossary/percentage-rollout/) is about *how you then reveal it*, expanding from a small cohort to everyone. The two compose: dark launch to land the code, then ramp it up gradually. See the [rollout strategies guide](/docs/concepts/rollout-strategies/) for how serving is configured. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Feature toggle Feature toggle is another name for a feature flag — a runtime switch that turns functionality on or off without a code deploy. The terms are interchangeable.](/glossary/feature-toggle/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/dogfooding/ # Dogfooding — Definition [Home](/) / [Glossary](/glossary/) / Dogfooding # Dogfooding Also called: eating your own dog food, internal beta Last updated: August 4, 2026 **Dogfooding** means using your own product, including the parts you have not released. Staff run the new code in their daily work, which surfaces friction and broken edges that a test suite and a staging environment both miss. None of that needs a separate build or an internal-only deployment. One targeting rule turns the feature on for your own accounts, in production and against production data, while every customer keeps the current behavior. ## How to target internal users The rule matches something only your team has. An email domain is the quickest version of this, using an `endsWith` condition on the email attribute, and an `isStaff` attribute your application already sets is the sturdier one, because it survives people signing up with personal addresses. Wrap either in a [segment](/glossary/segment-targeting/) and every flag can reuse the same internal audience without restating the rule. The [segments guide](/docs/guides/segments/) walks through building one. ## Where dogfooding sits in a rollout A bad build here costs an afternoon of your own team's time, which is what makes the first exposure the cheapest one to get wrong. Catch anything embarrassing at this stage, before a [beta](/glossary/beta-testing/) puts the feature in front of real users and well before a [percentage rollout](/glossary/percentage-rollout/) widens it to everyone. Your staff are also unrepresentative, and that caps what the stage can tell you. They know the product too well to hit the confusions a new user will, so expect this stage to surface bugs while usability problems survive it. Want the full picture? Read the concept guide: [Segments →](/docs/guides/segments/) ## Related terms [### Beta testing Beta testing puts a pre-release feature in front of a limited group of real users to gather feedback and find defects under real conditions, before it reaches everyone.](/glossary/beta-testing/) [### Targeted rollout A targeted rollout releases a feature to a specific audience — defined by user attributes or a segment — rather than to a random percentage of everyone, so the right users get it first.](/glossary/targeted-rollout/) [### Canary release A canary release exposes a new version to a small group of users first and watches its metrics before rolling it out to everyone — so problems surface on a fraction of traffic instead of the whole fleet.](/glossary/canary-release/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/experimentation/ # Experimentation — Definition [Home](/) / [Glossary](/glossary/) / Experimentation # Experimentation Also called: online experimentation Last updated: September 18, 2026 **Experimentation** is the discipline of validating changes with controlled experiments instead of opinion. You form a hypothesis, expose variations to randomly assigned users, measure a predefined metric, and keep the variation that wins by a statistically significant margin. [A/B testing](/glossary/a-b-testing/) is its most common form; multivariate and bandit tests extend the same idea. ## Where feature flags fit Experimentation needs three things: a way to assign users to variations, a way to keep that assignment stable, and a way to measure outcomes. Feature flags supply the first two — a flag serves each user a variation, and a deterministic hash keeps them in their group for the duration — so the same flag that controls a release can also hold a clean experimental split. The measurement itself comes from the metrics you already track against each variation. ## Experimentation vs feature management They overlap but optimize for different goals. [Feature flags](/glossary/feature-flag/) and the [feature management](/glossary/feature-management/) layer around them exist to control *release* — decoupling deploy from launch, ramping safely, killing bad code. Experimentation exists to control *learning* — deciding which version to keep. The same flag can serve both: ship a feature gradually *and* measure its effect on the metric you care about. ## What makes an experiment controlled Four properties separate an experiment from a change you happened to measure afterwards. Assignment has to be random, so that the groups differ in the treatment and in nothing else that matters. The metric and the effect size have to be fixed before the test starts, because choosing them once the data is in lets you find a winner in noise. The sample size follows from that effect size, and it decides how long the test runs. Fix the stopping point in advance as well. Checking every morning and stopping the first time the line crosses is the most common way a team talks itself into a result that will not reproduce. ## Why so many experiments end inconclusive Underpowered tests are the usual reason, and the arithmetic behind it is unforgiving. Detecting a one percent lift takes far more traffic than detecting a ten percent one, and most product changes move the metric by small amounts. A test that ends with no significant difference has still told you something true, which is that the effect is smaller than the size you powered for. The expensive version is the test that was never large enough to detect the effect you were hoping for, because it costs a release cycle and returns nothing either way. Work out the required sample before you build the variation. When the number is out of reach, ship the change behind a [percentage rollout](/glossary/percentage-rollout/) and watch your error rates and [flag](/glossary/feature-flag/) metrics for regressions instead of claiming a lift you cannot measure. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess.](/glossary/a-b-testing/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/feature-flag/ # Feature flag — Definition [Home](/) / [Glossary](/glossary/) / Feature flag # Feature flag Also called: feature toggle, feature switch, feature gate Last updated: July 7, 2026 A **feature flag** is a conditional wrapped around a piece of functionality whose value is decided by [remote configuration](/glossary/remote-config/) rather than hard-coded. An SDK evaluates the flag against the current user and the configuration on the server, returning a [variation](/glossary/variation/) — boolean, string, number, or JSON. Because the value lives in configuration, you change what your application does by editing the flag, not by shipping a release. ## How a feature flag works In code, a flag is a branch: `if (client.boolVariation("new-checkout", user, false))`. The SDK holds the flag configuration in memory and evaluates locally, so the check is a sub-millisecond lookup with no per-request network hop. When you change the flag in the dashboard, the new configuration is pushed to every connected SDK over a streaming connection and the next evaluation uses it — fleet-wide, with no redeploy. ## Why teams use feature flags Flags decouple *deploy* from *release*: you can ship code to production with the feature off, then turn it on when you choose. They make [percentage rollouts](/glossary/percentage-rollout/) and gradual exposure a configuration change, give you a [kill switch](/glossary/kill-switch/) for risky code, and let you serve different variations to beta users or internal staff. For the full treatment of variations, environments, and the evaluation model, see the [feature flags concept guide](/docs/concepts/feature-flags/) . Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature toggle Feature toggle is another name for a feature flag — a runtime switch that turns functionality on or off without a code deploy. The terms are interchangeable.](/glossary/feature-toggle/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required.](/glossary/kill-switch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/feature-management/ # Feature management — Definition [Home](/) / [Glossary](/glossary/) / Feature management # Feature management Also called: feature flag management Last updated: July 7, 2026 **Feature management** is the discipline of running [feature flags](/glossary/feature-flag/) as a managed system rather than scattered `if` statements. It means one place to define flags and their [variations](/glossary/variation/) , decide who sees each variation, ramp a rollout, and retire a flag once its job is done. A *feature management platform* (also called a feature flag service) is the tool that provides this: the dashboard, the SDKs, and the evaluation engine behind them. ## What a feature management platform does Four things a bare boolean in a config file does not: it targets variations by user, attribute, or [segment](/glossary/segment-targeting/) rather than serving everyone the same value; it ramps exposure with a [percentage rollout](/glossary/percentage-rollout/) and cuts it with a [kill switch](/glossary/kill-switch/) ; it keeps a separate configuration per environment with an audit trail of every change; and it ships SDKs that evaluate locally in sub-milliseconds and receive updates over a streaming connection. The [feature flags concept guide](/docs/concepts/feature-flags/) covers the full model. ## Feature management vs a homegrown flag system Most teams start with a database column and an `if` statement, which works until the questions get harder: how do we target one customer, keep staging and production separate, see who flipped a flag last week, update every server without a redeploy, or find the flags nobody uses anymore? Feature management is the point where the flag system itself becomes a product surface worth not building in-house. The trade-off is laid out in [build versus buy for feature flags](/blog/build-vs-buy-feature-flags/) . If the worry is committing to one vendor, the [OpenFeature](/glossary/openfeature/) standard lets you write against a vendor-neutral evaluation API and keep the platform swappable. The administration side has its own emerging interface: an [MCP server](/glossary/model-context-protocol/) exposes the same management operations as tools an AI editor can call. Those operations can also be declared in a file instead of clicked, which is what [flags as code](/glossary/flags-as-code/) means. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Remote config Remote config stores application settings on a server so you can change them without shipping a release, using the same delivery mechanism a feature flag does but for values like limits, copy, and thresholds rather than on/off decisions.](/glossary/remote-config/) [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/feature-toggle/ # Feature toggle — Definition [Home](/) / [Glossary](/glossary/) / Feature toggle # Feature toggle Also called: feature flag, feature switch Last updated: September 18, 2026 A **feature toggle** is the same thing as a [feature flag](/glossary/feature-flag/) : a switch that changes application behavior at runtime without deploying new code. "Toggle" and "flag" are interchangeable — there is no technical difference between them. ## Flag or toggle? "Feature toggle" is the older term, popularized by trunk-based-development and continuous-delivery practitioners; "feature flag" spread later through SaaS vendors and is more common today. You will also see "feature switch" and "feature gate". The distinction that actually matters is not the word but the *kind* of toggle — release toggles (short-lived, for decoupling deploy from release), experiment toggles (A/B tests), ops toggles ([kill switches](/glossary/kill-switch/) ), and permission toggles (tier gating) — a taxonomy that describes lifespan and purpose, not naming. ## Does the name change anything in practice? No. The workflow is identical whichever word your team uses: wrap functionality in a conditional, configure the value remotely, and change behavior without touching code. The practical advice is to pick one term and stay consistent in your own codebase and documentation so searches and onboarding stay simple. ## The four kinds of toggle **Release toggles** hide work in progress so a half-finished feature can sit on the main branch without reaching users. They should live for days or weeks and then come out. **Experiment toggles** split traffic to measure which version performs better, and they last as long as the test does. **Ops toggles** are the [kill switches](/glossary/kill-switch/) and load shedders an on-call engineer reaches for, and some of those are meant to stay for the life of the system. **Permission toggles** gate functionality by plan or role, and they are effectively permanent, because they encode a product rule rather than a temporary decision. The categories come from Pete Hodgson's [feature toggles article](https://martinfowler.com/articles/feature-toggles.html) , and the reason to know which kind you are holding is that only the first two are ever supposed to be removed. Treating a permission toggle as [flag debt](/glossary/flag-debt/) and deleting it takes a paying customer's feature away. ## What a feature toggle service does Storing a boolean somewhere central is the easy part, and most teams build that version themselves first. A hosted service adds the parts that get awkward later: per-environment values so staging and production can differ, targeting rules so a toggle can be on for one cohort and off for everyone else, a record of who changed what and when, and delivery into running processes within seconds of a change rather than at the next deploy. [Feature management](/glossary/feature-management/) is the usual name for that whole layer. The move usually happens when a config file stops being able to answer who turned this off on Tuesday, or when the first toggle needs to be on for one customer only. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [### Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required.](/glossary/kill-switch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/flag-debt/ # Flag debt — Definition [Home](/) / [Glossary](/glossary/) / Flag debt # Flag debt Also called: feature-flag debt, toggle debt Last updated: September 4, 2026 **Flag debt** is the form of technical debt created when feature flags pile up faster than they are retired. Every flag is a branch in the code and a row in the configuration; once its decision is made — the rollout finished, the experiment concluded — the flag becomes dead weight that engineers still have to read around, test, and reason about. Flag debt is the sum of those un-retired flags. ## Where flag debt comes from Most flags are temporary by intent but permanent by neglect. A [percentage rollout](/glossary/percentage-rollout/) reaches 100% and nobody deletes the flag; an [A/B test](/glossary/a-b-testing/) ends and the losing branch lingers; a feature is abandoned behind an off switch. Each survivor is a [stale flag](/glossary/stale-flag/) , and stale flags are what flag debt is made of. The cost compounds: more branches to test, more states to reason about, and a real outage risk when someone misreads which path is live — the Knight Capital trading failure began with logic left behind a flag that should have been retired. ## Paying flag debt down The fix is lifecycle discipline: treat every temporary flag as carrying a removal task, and make retiring flags routine rather than heroic. The hard part is knowing which flags are safe to remove, so Featureflip classifies each one as Active, Stale, or Dead from real [flag evaluation](/glossary/flag-evaluation/) traffic — the cleanup queue builds itself instead of waiting on an audit. See the [stale flag detection guide](/docs/concepts/stale-flags/) for the model, and [a practical flag-cleanup workflow](/blog/feature-flag-cleanup/) for retiring them safely. Where that removal is handed to automation, the useful question becomes [which rewrites a tool should decline to make](/blog/automated-feature-flag-removal/) . Want the full picture? Read the concept guide: [Stale flag detection →](/docs/concepts/stale-flags/) ## Related terms [### Stale flag A stale flag is a feature flag that no longer makes a live decision — it serves one variation to everyone, receives no traffic, or was never used — yet still exists as a conditional branch in code.](/glossary/stale-flag/) [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/flag-evaluation/ # Flag evaluation — Definition [Home](/) / [Glossary](/glossary/) / Flag evaluation # Flag evaluation Also called: feature flag evaluation Last updated: June 28, 2026 **Flag evaluation** is the moment a feature flag produces a value. Given a flag's configuration and a user context, the SDK works through the flag's logic — prerequisites first, then targeting rules, then any [percentage rollout](/glossary/percentage-rollout/) , then the default — and returns the variation the user should see, along with a reason explaining why. ## Local evaluation vs calling an API There are two models. In *local evaluation*, the SDK holds the flag configuration in memory and computes the result itself — a sub-millisecond lookup with no per-check network call — and receives configuration updates over a streaming connection. In *remote evaluation*, the SDK asks a server for each result, adding network latency to every check. Featureflip's server-side SDKs evaluate locally; the [reliability guide](/docs/concepts/reliability/) covers how they fail safe when the service is unreachable. The model also shapes cost: vendors that bill per client-side MAU or per flag request meter evaluation volume itself, which is why teams whose traffic outgrew that bill weigh the [LaunchDarkly alternatives](/alternatives/launchdarkly-alternative/) that price evaluation differently. ## What an evaluation decides on Evaluation runs against the user context you pass — identifier, attributes, and group membership — checking [segment targeting](/glossary/segment-targeting/) and rules in order. The order matters: a failed prerequisite short-circuits the rest, and the first matching rule wins. Each result carries a reason — rule match, fallthrough, prerequisite failed — so you can see exactly why a user got the variation they did. This same deterministic machinery is what makes [A/B testing](/glossary/a-b-testing/) and gradual rollouts repeatable. The call itself can be made through a vendor SDK or through the vendor-neutral [OpenFeature](/glossary/openfeature/) API, which standardizes the evaluation call while leaving these semantics to the underlying service. Want the full picture? Read the concept guide: [Reliability & resilience →](/docs/concepts/reliability/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Segment targeting Segment targeting serves a flag variation to a named, reusable group of users defined by shared attributes — like "beta users" or "EU customers" — instead of listing individuals one by one.](/glossary/segment-targeting/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/flags-as-code/ # Flags as code — Definition [Home](/) / [Glossary](/glossary/) / Flags as code # Flags as code Also called: feature flags as code, flag configuration as code Last updated: August 1, 2026 **Flags as code** is the practice of keeping feature flag configuration in version-controlled files rather than only in a dashboard. What gets declared is structure: which flags exist, what [variations](/glossary/variation/) they carry, which [segments](/glossary/segment-targeting/) they target, and what their targeting rules say. A tool reads those files and makes the flag platform match, so a targeting change arrives as a diff with a reviewer and a permanent history behind it. The workflow around the practice is usually called GitOps for feature flags. ## What gets declared, and what stays out Two things stay out of the files deliberately. The first is [evaluation](/glossary/flag-evaluation/) : reading a flag's value for a given user happens in your application through an SDK, in memory, on the request path, and no configuration file is involved. The second is fast toggling. Turning a flag off during an incident has to be immediate, and routing it through a pipeline adds minutes to recovery for no gain, so the [kill switch](/glossary/kill-switch/) stays on the dashboard or the API. What is left is the long-lived structure, which is exactly the part that accumulates and benefits from review. A [walkthrough of the workflow](/blog/gitops-feature-flags/) covers the review checklist and the CI pipeline, and a [worked Terraform setup](/blog/feature-flags-as-code-terraform/) covers the resources and the import path. ## Drift is the hard edge Drift is any difference between what the files declare and what the platform is serving. Infrastructure tooling treats that as a defect and corrects it on a schedule, which is the wrong instinct here: a flag someone turned off at 3am is drift, and reverting it automatically would undo an incident response. Two habits follow. Declare only what you are willing to have reverted, because rules are usually authoritative once managed, and a rule added in the dashboard disappears on the next apply. And reconcile on a decision rather than a timer, folding real drift back in as a pull request afterwards. Most established flag platforms offer a Terraform provider or an equivalent API for this, and they differ mainly on who can reach it, since some reserve it for higher tiers. Featureflip's [Terraform provider](/docs/integrations/terraform/) and [management](/glossary/feature-management/) API work on every plan including the free one. Want the full picture? Read the concept guide: [Environments →](/docs/concepts/environments/) ## Related terms [### Feature management Feature management is the practice, and the category of tools, for running feature flags as a system: defining, targeting, rolling out, and retiring them from one place without redeploying.](/glossary/feature-management/) [### Segment targeting Segment targeting serves a flag variation to a named, reusable group of users defined by shared attributes — like "beta users" or "EU customers" — instead of listing individuals one by one.](/glossary/segment-targeting/) [### Model Context Protocol (MCP) The Model Context Protocol is an open standard for giving AI assistants callable tools, and an MCP server for feature flags lets an assistant like Claude Code or Cursor create, toggle, and clean up flags without leaving the editor.](/glossary/model-context-protocol/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/kill-switch/ # Kill switch — Definition [Home](/) / [Glossary](/glossary/) / Kill switch # Kill switch Also called: ops toggle Last updated: June 10, 2026 A **kill switch** is a [feature flag](/glossary/feature-flag/) whose job is to switch a feature *off* fast. When something goes wrong in production, you set the flag to off and every connected SDK serves the fallback on its next evaluation — within seconds, with no deployment, rebuild, or rollback. ## When you reach for a kill switch Common triggers: a third-party dependency starts timing out, a new feature spikes database load, or a bug surfaces only under production traffic. Wrapping risky or resource-heavy behavior in a flag means the mitigation is a single switch rather than an incident-length revert. Teams often pair a kill switch with a [percentage rollout](/glossary/percentage-rollout/) so they can both ramp exposure up and cut it to zero instantly. For a fuller walkthrough of what to wrap and how to choose a fail-safe default, see [using a kill switch in production](/use-cases/kill-switch/) . ## Kill switch vs rollback A rollback reverts a deployment: it is slower (rebuild and redeploy), coarse (it reverts everything in that release, not just the broken feature), and risky during an incident. A kill switch is scoped to one feature and takes effect on the next flag evaluation. The new code stays in the binary — no traffic executes it — so you can triage the root cause without the clock running. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/model-context-protocol/ # Model Context Protocol (MCP) — Definition [Home](/) / [Glossary](/glossary/) / Model Context Protocol (MCP) # Model Context Protocol (MCP) Also called: MCP, MCP server Last updated: July 31, 2026 **Model Context Protocol (MCP)** is an open standard that describes how an AI assistant discovers and calls external tools. An **MCP server** is a small program that publishes a set of named operations, and any MCP-aware client can then call them on the user's behalf. Applied to feature flags, it means the assistant that just wrote the code behind a flag can also create that flag, enable it in one environment, ramp its rollout, and delete it once the feature has shipped. ## How a flag MCP server fits together Three parts. At the front, the **client** is whatever AI tool you already use, such as Claude Code, Cursor, VS Code Copilot, or an autonomous agent. Behind it an **MCP server** publishes the tools and does the translating, and underneath that sits the ordinary **flag platform API** that the dashboard and your own scripts already call, so nothing the agent does happens on a private side channel. Featureflip's server (`@featureflip/mcp`) runs on demand through `npx` with no service to deploy, authenticates with the same bearer token as the REST API, and turns each tool call into an ordinary API request that is audit-logged and attributed to that token. The [MCP server guide](/docs/integrations/mcp/) lists every tool, and a [walkthrough of the three agent workflows](/blog/feature-flags-mcp-server/) covers wrapping new code, ramping a rollout, and cleaning up [stale flags](/glossary/stale-flag/) . ## Configuration is its whole job An MCP server sits on the configuration plane. It creates flags, sets variations, toggles environments, and writes targeting rules, all of which are deliberate low-frequency changes. That same plane can be driven declaratively instead, from files under version control, which is the [flags as code](/glossary/flags-as-code/) approach. It does not evaluate flags. Your application still reads a value for a given user through an SDK on the request path, in memory, which is the split [feature management](/glossary/feature-management/) platforms draw between administering a flag and reading one. Most established flag vendors now ship an MCP server, so the interface itself has become ordinary. They differ on two things worth checking: who can reach it, since some gate the platform behind a paid tier, and how carefully the tool set is scoped. Featureflip leaves the foot-guns out entirely, with no tool to mint SDK keys or delete a project, and marks destructive tools so the client asks before running them. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature management Feature management is the practice, and the category of tools, for running feature flags as a system: defining, targeting, rolling out, and retiring them from one place without redeploying.](/glossary/feature-management/) [### Stale flag A stale flag is a feature flag that no longer makes a live decision — it serves one variation to everyone, receives no traffic, or was never used — yet still exists as a conditional branch in code.](/glossary/stale-flag/) [### OpenFeature OpenFeature is a CNCF open standard that defines one vendor-neutral API for evaluating feature flags, with a swappable provider behind it, so changing flag vendors becomes a configuration change instead of a rewrite.](/glossary/openfeature/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/multivariate-testing/ # Multivariate testing — Definition [Home](/) / [Glossary](/glossary/) / Multivariate testing # Multivariate testing Also called: MVT, multivariate test Last updated: September 18, 2026 **Multivariate testing** (MVT) runs an experiment with three or more variations, or with combinations of several independent changes tested together, rather than the two-way split of an [A/B test](/glossary/a-b-testing/) . It measures which variation — or which combination of elements — moves the target metric the most. ## How it differs from an A/B test An A/B test compares one control against one treatment. Multivariate testing either compares many discrete variations (A/B/C/D) or factorially combines elements — say two headlines crossed with two button colors, giving four cells — to measure both the individual effects and how they interact. The trade-off is sample size: more cells split your traffic thinner, so each needs more users to reach significance, which is why MVT suits higher-traffic surfaces. ## How feature flags deliver it A multivariate flag serves one of several variations — string, number, or JSON — to each user, assigned by a deterministic hash so the split stays stable for the life of the test (the same [percentage rollout](/glossary/percentage-rollout/) bucketing, divided into more than two buckets); the [rollout strategies guide](/docs/concepts/rollout-strategies/) covers the serving model. The flag controls assignment; your analytics measures each cell. Featureflip handles the delivery side and leaves the statistics to your analytics stack — the same division of labor described under [experimentation](/glossary/experimentation/) . ## How many combinations a multivariate test creates Every element you add multiplies the cells rather than adding to them. Three headlines crossed with two images and two button colors produces twelve variations, because the test serves every combination. Compute that number before anything else. A *full factorial* design covers all twelve cells and can tell you whether the headline and the button interact, which is the question a sequence of [A/B tests](/glossary/a-b-testing/) cannot answer. A *fractional factorial* design samples a subset of the cells and infers the rest, trading some of that interaction detail for a test that actually finishes. ## How much traffic multivariate testing needs Traffic decides whether this technique is available to you at all. Each cell needs enough users to reach significance on its own, so a twelve-cell test needs roughly twelve times the sample of a two-arm test aimed at the same effect size. On a page taking a few hundred sessions a week, that runs well past the point where the answer would still be useful, and seasonality starts to contaminate the result. Work out the per-cell sample first, divide your weekly traffic by the number of cells, and read off how many weeks the design implies. If the answer is more than about four, run [sequential A/B tests](/glossary/a-b-testing/) on the elements you believe matter most and accept that you will learn less about how they interact. ## When to run one instead of sequential A/B tests Reach for a multivariate test when you suspect the elements affect each other. A bolder headline might pay off only next to the larger hero image, and a run of single-element tests would miss that, because each one holds everything else fixed at whatever the current design happens to be. Sequential tests win when traffic is tight, when the changes are plainly independent, or when you need an answer this month. The [experimentation](/glossary/experimentation/) discipline is identical either way. Only the design and its traffic cost differ. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess.](/glossary/a-b-testing/) [### Experimentation Experimentation is the practice of making product decisions from controlled experiments — running A/B and multivariate tests, measuring outcomes against a hypothesis, and shipping the version the data supports rather than the one with the loudest advocate.](/glossary/experimentation/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/openfeature/ # OpenFeature — Definition [Home](/) / [Glossary](/glossary/) / OpenFeature # OpenFeature Also called: OpenFeature standard, OpenFeature provider Last updated: July 29, 2026 **OpenFeature** is a vendor-neutral specification, hosted by the Cloud Native Computing Foundation, for how applications evaluate feature flags. Your code calls a standard API (`getBooleanValue`, `getStringValue`, and friends) against an OpenFeature SDK, and a **provider** plugged in behind it translates those calls for a specific flag service. Because the call sites target the standard rather than a vendor SDK, swapping the provider swaps the backend without touching the code that reads flags. ## How the provider model works There are three pieces. The **OpenFeature SDK** for your language exposes the standard evaluation API and holds the evaluation context. A **provider** is a thin adapter that implements the spec against one flag service. And your application sets the provider once at startup, then calls the standard API everywhere else. The provider does the real work of resolving a [variation](/glossary/variation/) , so [flag evaluation](/glossary/flag-evaluation/) semantics like targeting, defaults, and evaluation reasons still come from the underlying service. Featureflip ships providers for Node.js, .NET, Python, Go, Java and PHP today, with more languages planned. The [OpenFeature integration guide](/docs/integrations/openfeature/) covers installation and context mapping for each, and a [worked setup for each runtime](/blog/openfeature-feature-flags/) walks through the provider code and the context field that drives rollout bucketing. ## Why teams adopt the standard The usual reason is switching cost. A proprietary SDK spreads vendor-specific calls through the codebase, so migrating means editing every call site; coding against OpenFeature confines the vendor to one line of setup. It also makes a gradual migration practical, since you can run one provider in one service while another is still live elsewhere, and it gives multi-language shops one evaluation vocabulary instead of a different SDK idiom per stack. The trade is that the standard covers the common surface, so vendor-specific extras may still need the native SDK. Note that OpenFeature standardizes evaluation only, not flag *management*: creating, targeting, and archiving flags stays with each vendor API, the same split [remote config](/glossary/remote-config/) draws between reading a value and administering it. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [### Remote config Remote config stores application settings on a server so you can change them without shipping a release, using the same delivery mechanism a feature flag does but for values like limits, copy, and thresholds rather than on/off decisions.](/glossary/remote-config/) [### Feature management Feature management is the practice, and the category of tools, for running feature flags as a system: defining, targeting, rolling out, and retiring them from one place without redeploying.](/glossary/feature-management/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/percentage-rollout/ # Percentage rollout — Definition [Home](/) / [Glossary](/glossary/) / Percentage rollout # Percentage rollout Also called: percentage-based rollout, gradual rollout Last updated: July 31, 2026 A **percentage rollout** exposes a feature to a configured share of users — say 10% — instead of everyone at once. Membership is decided by a deterministic hash of the user identifier and the flag key, so the assignment is stable: the same user always lands in the same bucket for the same flag. ## How the bucketing works The SDK hashes the user identifier together with the flag key into a stable bucket from 0–99. A 10% rollout serves the new variation to buckets 0–9. Because the hash is deterministic, raising the percentage to 25% *adds* users to the enabled cohort without re-randomizing the original 10% — a property called [sticky bucketing](/glossary/sticky-bucketing/) . This is what makes a rollout safe to ramp and safe to compose with experiments. The mechanics are covered in the [rollout strategies guide](/docs/concepts/rollout-strategies/) . ## Percentage rollout vs progressive rollout A percentage rollout is the *mechanism* — a flag set to serve N% of traffic. A [progressive rollout](/use-cases/progressive-rollouts/) is the *practice* of stepping that percentage up over time (1% → 5% → 25% → 100%) while watching metrics at each tier. You need a percentage rollout to run a progressive one; the percentage is the dial, the progression is how you turn it. ## Limits of the percentage The dial controls *how many* users see the feature. Which users get it is a separate question, answered by a [targeted rollout](/glossary/targeted-rollout/) and configured as a rule on the flag: internal staff first, or the accounts in one region. They stack. A rule picks the audience and a percentage ramps inside it. Consistent bucketing depends on a stable identifier arriving in the evaluation context, without which an anonymous caller cannot be held in one cohort from request to request. Exposure is all the percentage buys, and whether the new variation actually performs better belongs to your analytics or [experimentation](/glossary/experimentation/) stack. See [how percentage rollouts work in Featureflip](/product/features/percentage-rollouts/) for the serving side. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [### Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required.](/glossary/kill-switch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/progressive-delivery/ # Progressive delivery — Definition [Home](/) / [Glossary](/glossary/) / Progressive delivery # Progressive delivery Last updated: June 3, 2026 **Progressive delivery** is an evolution of continuous delivery that adds two controls on top of "the code is in production": *progressive rollout* — exposure expands in stages rather than all at once — and *progressive delegation* — control over each release shifts from engineering toward product and support as confidence grows. Feature flags, [canary releases](/glossary/canary-release/) , and [percentage rollouts](/glossary/percentage-rollout/) are its building blocks. ## The two halves: progressive rollout and progressive delegation The term, coined by RedMonk analyst James Governor in 2018, named a pattern teams were already converging on. The first half, progressive rollout, means a release reaches users in widening rings — internal staff, then a [canary](/glossary/canary-release/) , then 10%, then everyone — each tier gated on healthy metrics. The second half, progressive delegation, means the power to flip a feature on moves outward over time: engineering ships it dark, then product decides when to reveal it, then support can toggle it per customer. Feature flags are what make both halves a configuration decision rather than a deployment. For the full treatment, including a technique-selection rubric and the DORA case, read [the two-pillar playbook](/blog/progressive-delivery-explained/) . ## How it differs from continuous delivery [Continuous delivery](/glossary/continuous-delivery/) gets code to production safely and often; it stops at deploy. Progressive delivery picks up where it ends, treating *release* as a separate, gradual, reversible act. In practice that means deploying behind a flag (a [dark launch](/glossary/dark-launch/) ), ramping with a [percentage rollout](/glossary/percentage-rollout/) , and keeping a kill switch ready. For a worked example of staging a rollout, see [progressive rollouts in practice](/use-cases/progressive-rollouts/) . Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Canary release A canary release exposes a new version to a small group of users first and watches its metrics before rolling it out to everyone — so problems surface on a fraction of traffic instead of the whole fleet.](/glossary/canary-release/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/remote-config/ # Remote config — Definition [Home](/) / [Glossary](/glossary/) / Remote config # Remote config Also called: remote configuration, remote config server, dynamic config Last updated: July 7, 2026 **Remote config** keeps values your application reads — timeouts, feature limits, thresholds, copy, endpoints — in server-side configuration an SDK fetches at runtime, so changing one is a dashboard edit rather than a deploy. A [feature flag](/glossary/feature-flag/) is a specialized kind of remote config whose value drives a branch in code; remote config is the broader category of any remotely controlled value. ## Remote config vs feature flags They answer different questions with the same machinery. A feature flag asks "should this code path run?" — usually a boolean, often targeted per user, frequently short-lived. Remote config asks "what value should this setting hold?" — any type, often global, usually long-lived. The line blurs because a Featureflip flag can return a number, string, or JSON [variation](/glossary/variation/) , so a JSON flag holding a config payload is remote config in all but name. Both are values delivered from a server and resolved by [evaluation](/glossary/flag-evaluation/) against the current context. ## How remote config is delivered The SDK holds the configuration in memory and reads it locally, so a lookup is sub-millisecond with no per-read network hop. When you change a value in the dashboard it is pushed to every connected SDK over a streaming connection, and the next read uses it — fleet-wide, within seconds, no redeploy. Because the same targeting applies, one key can resolve to different values per [segment](/glossary/segment-targeting/) or environment. The [feature flags concept guide](/docs/concepts/feature-flags/) describes the evaluation model these values share. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Feature toggle Feature toggle is another name for a feature flag — a runtime switch that turns functionality on or off without a code deploy. The terms are interchangeable.](/glossary/feature-toggle/) [### Feature management Feature management is the practice, and the category of tools, for running feature flags as a system: defining, targeting, rolling out, and retiring them from one place without redeploying.](/glossary/feature-management/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/segment-targeting/ # Segment targeting — Definition [Home](/) / [Glossary](/glossary/) / Segment targeting # Segment targeting Also called: audience targeting Last updated: June 20, 2026 **Segment targeting** evaluates a feature flag against a *segment*: a reusable, named group of users defined by rules over their attributes rather than an ad-hoc list. Define "internal staff" or "enterprise plan" once, then point any flag at that segment — membership is computed at [evaluation](/glossary/flag-evaluation/) time from the user context you pass in. ## Segments vs inline targeting rules You can target a flag two ways. An inline targeting rule lives on a single flag — "serve true when plan is pro". A segment factors that audience out so many flags can share it: change the segment definition once and every flag pointed at it updates. Segments are the right tool when the same audience recurs across flags; inline rules suit one-off conditions. In Featureflip, segments use flat conditions with AND/OR logic while flag rules use condition groups — see the [user targeting and segments guide](/docs/concepts/targeting/) . ## How segment targeting is evaluated At evaluation, the SDK checks the user context — id, plan, region, custom attributes — against each segment's conditions and each flag rule, then serves the matching variation. Because it runs locally against in-memory configuration, the check is sub-millisecond. Segment targeting composes with a [percentage rollout](/glossary/percentage-rollout/) : target a segment, then roll out to a percentage *within* it, which is the basis of a [targeted rollout](/glossary/targeted-rollout/) . Want the full picture? Read the concept guide: [User targeting & segments →](/docs/concepts/targeting/) ## Related terms [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Targeted rollout A targeted rollout releases a feature to a specific audience — defined by user attributes or a segment — rather than to a random percentage of everyone, so the right users get it first.](/glossary/targeted-rollout/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/server-side-testing/ # Server-side testing — Definition [Home](/) / [Glossary](/glossary/) / Server-side testing # Server-side testing Also called: server-side experimentation Last updated: August 4, 2026 **Server-side testing** is any test or rollout whose variation is chosen in your own backend, before the response reaches the user. A server SDK holds the flag configuration in memory and evaluates locally, so the decision costs a sub-millisecond lookup and no extra network call. The rule that produced it never leaves your infrastructure, which is why anything a user should not be able to read or influence belongs on this side. ## What a server key can see A server key, prefixed `sdk_server_`, reads every flag in the project. It must never reach a browser. That access is what lets a backend-only flag gate an unshipped API, a schema migration, or a pricing rule while both its name and its payload stay private. The [SDK overview](/docs/sdks/overview/) sets the two key types out side by side. ## What this changes about a test The assignment is made once, in a place you control, on data you already hold. Nothing flashes the wrong variation while a client bootstraps, because the response arrives already decided. Attributes that never reach the browser can drive the rule, which covers most of the interesting ones, from an account's contract value to an internal risk tier. The trade-off shows up when a change affects only rendering, because deciding it still costs a request, so teams commonly evaluate on the server and pass the result down to the [client](/glossary/client-side-testing/) rather than deciding it twice. Want the full picture? Read the concept guide: [SDK overview →](/docs/sdks/overview/) ## Related terms [### Client-side testing Client-side testing decides the variation inside the user's browser or mobile app, using a client SDK and a client key that can read only the flags you have marked visible to the client.](/glossary/client-side-testing/) [### A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess.](/glossary/a-b-testing/) [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/stale-flag/ # Stale flag — Definition [Home](/) / [Glossary](/glossary/) / Stale flag # Stale flag Also called: zombie flag, dead flag Last updated: September 14, 2026 A **stale flag** is a [feature flag](/glossary/feature-flag/) that has outlived its decision. The rollout finished, the experiment picked a winner, or the code path stopped running — but the flag and its conditional are still in the codebase. Each one is a branch engineers must keep reading, testing, and debugging around, which is why stale flags are the dominant form of feature-flag technical debt. ## How flags go stale Most flags are meant to be temporary, and they go stale in predictable ways: a [percentage rollout](/glossary/percentage-rollout/) reaches 100% and nobody deletes the flag; an experiment ends and the losing variation lingers as dead code; a feature is rolled back and abandoned; or a flag is created and never wired into the application. The exception is intentionally permanent flags — a [kill switch](/glossary/kill-switch/) serves one variation for months by design, which is why staleness should prompt a review rather than an automatic delete. ## How Featureflip detects stale flags Featureflip classifies every flag in every environment as Active, **Stale** (a staleness signal has persisted for 30 days), or **Dead** (90 days) by analyzing real evaluation traffic — no codebase scan or configuration required. Each status carries the reasons: fully rolled out, rolled back, no traffic, or never used. See the [stale flag detection guide](/docs/concepts/stale-flags/) for the full model and cleanup workflow. Detection models differ across vendors: time-based reports such as ConfigCat's zombie flags report key off when a flag was last changed, which misses flags that are still touched but serve one variation to everyone. Flagsmith's stale-flag detection, on Scale-Up and above, is time-based in the same way. The [guide to ConfigCat alternatives](/alternatives/configcat-alternative/) and the [Flagsmith alternatives guide](/alternatives/flagsmith-alternative/) compare staleness tooling across platforms. Once a flag reads as finished, an [MCP server](/glossary/model-context-protocol/) lets an AI editor find it, strip the dead branch, and archive it in one pass. A scheduled CI job can also [propose the removal as a pull request](/blog/automated-feature-flag-removal/) without anyone opening an editor. Want the full picture? Read the concept guide: [Stale flag detection →](/docs/concepts/stale-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Kill switch A kill switch is a feature flag used to disable functionality instantly in production — flip it off and every SDK stops running the risky code path within seconds, no redeploy required.](/glossary/kill-switch/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/sticky-bucketing/ # Sticky bucketing — Definition [Home](/) / [Glossary](/glossary/) / Sticky bucketing # Sticky bucketing Also called: consistent bucketing, deterministic bucketing Last updated: July 7, 2026 **Sticky bucketing** — also called deterministic or consistent bucketing — is how a feature flag keeps a user's assignment stable. Rather than drawing a fresh random number on each check, the SDK hashes the user identifier together with the flag key into a fixed bucket from 0 to 99. The hash is pure, so the same user and the same flag always produce the same bucket, on every server and every request. ## Why stickiness matters Without it, a 10% [percentage rollout](/glossary/percentage-rollout/) would re-roll on every request and a user would flicker in and out of the feature — a jarring experience and useless experiment data. Because the bucket is deterministic, raising the rollout from 10% to 25% only *adds* users to the enabled cohort; the original 10% never move. The same property lets an [A/B test](/glossary/a-b-testing/) hold a clean, stable split for the life of the experiment. ## How the bucket is computed The SDK hashes the user identifier and the flag key together and maps the result onto 0–99. Folding the flag key into the input means one user gets independent buckets across different flags, so unrelated rollouts never correlate. The computation runs locally against in-memory configuration, so it is sub-millisecond, needs no coordination between servers, and produces the same deterministic result across every Featureflip SDK. It is the mechanism a [targeted rollout](/glossary/targeted-rollout/) layers on top of when it rolls out to a percentage within a chosen audience; the [rollout strategies guide](/docs/concepts/rollout-strategies/) covers the serving model. Want the full picture? Read the concept guide: [Rollout strategies →](/docs/concepts/rollout-strategies/) ## Related terms [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### A/B testing A/B testing serves two variations to randomly assigned groups of users and measures which one performs better on a chosen metric — turning a product decision into a controlled experiment instead of a guess.](/glossary/a-b-testing/) [### Targeted rollout A targeted rollout releases a feature to a specific audience — defined by user attributes or a segment — rather than to a random percentage of everyone, so the right users get it first.](/glossary/targeted-rollout/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/targeted-rollout/ # Targeted rollout — Definition [Home](/) / [Glossary](/glossary/) / Targeted rollout # Targeted rollout Also called: targeted release, audience-based rollout Last updated: July 31, 2026 A **targeted rollout** exposes a feature to a chosen audience selected by who users are — their plan, region, role, or membership of a [segment](/glossary/segment-targeting/) — instead of to a random slice of the whole user base. Where a [percentage rollout](/glossary/percentage-rollout/) asks "what fraction of everyone?", a targeted rollout asks "which users?". ## Deliberate selection by attribute With a targeted rollout you name the audience up front, and the flag serves whoever matches. A [percentage rollout](/glossary/percentage-rollout/) samples across all traffic at random. The two compose. You target an audience and then roll out to a share *within* it ("enable for 10% of EU enterprise accounts"), which is the shape most real releases take: a [canary](/glossary/canary-release/) aimed at internal staff first, then widened. The [targeting rules guide](/docs/guides/targeting-rules/) shows how the serving is configured. ## When to use a targeted rollout Reach for it when exposure should follow the user, not chance: shipping to [beta opt-ins](/use-cases/beta-programs/) , enabling a feature only for the plan tier that paid for it ([entitlement gating](/use-cases/entitlements/) with a [feature flag](/glossary/feature-flag/) ), [dogfooding](/glossary/dogfooding/) with internal accounts, or honoring data residency by region. Because membership is recomputed at each [evaluation](/glossary/flag-evaluation/) , users move in and out of the target automatically as their attributes change, with no redeploy and no manual list-keeping. ## How the audience is resolved at evaluation time A targeted rollout is an ordered list of rules on the flag. Each rule pairs an attribute (plan, country, email, or any custom property your app sets) with an operator such as `equals`, `in`, `contains`, or `startsWith`, and a variation to serve on a match. Rules are checked top to bottom and the first match wins, which makes order a precedence list, and anything matching no rule falls through to the flag's default. Narrow overrides go first. The attributes are never stored by the flag service. Your application passes them in the evaluation context on every call, which is why a customer who upgrades their plan is targeted correctly on the very next request. Audiences reused across several flags belong in a named [segment](/glossary/segment-targeting/) instead, where widening one cohort becomes a single edit that every flag picks up. Rules are also scoped per environment, which keeps a rollout targeted in staging off in production until you say otherwise. Want the full picture? Read the concept guide: [User targeting & segments →](/docs/concepts/targeting/) ## Related terms [### Percentage rollout A percentage rollout serves a feature to a defined share of users — chosen by a deterministic hash — so the same users stay in the cohort as you ramp the percentage up.](/glossary/percentage-rollout/) [### Segment targeting Segment targeting serves a flag variation to a named, reusable group of users defined by shared attributes — like "beta users" or "EU customers" — instead of listing individuals one by one.](/glossary/segment-targeting/) [### Canary release A canary release exposes a new version to a small group of users first and watches its metrics before rolling it out to everyone — so problems surface on a fraction of traffic instead of the whole fleet.](/glossary/canary-release/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/trunk-based-development/ # Trunk-based development — Definition [Home](/) / [Glossary](/glossary/) / Trunk-based development # Trunk-based development Also called: TBD Last updated: May 28, 2026 **Trunk-based development** is a source-control practice in which developers integrate to one shared branch — the trunk, often `main` — in small, frequent commits, rather than working on long-lived feature branches that merge weeks later. The trunk is kept releasable at all times, which is what makes continuous integration and continuous delivery practical. ## Why trunk-based development needs feature flags Committing to trunk constantly raises an obvious problem: how do you merge code for a feature that is not finished? The answer is to merge it [dark](/glossary/dark-launch/) — wrapped in a [feature flag](/glossary/feature-flag/) held off. The incomplete work lives on trunk, integrates continuously with everyone else's changes, and stays invisible until the flag is turned on. Without flags, teams fall back to long-lived branches and the painful big-bang merges trunk-based development exists to avoid. What that costs in practice, and which flag states a pipeline has to run once unfinished work lives on trunk, is covered in [how trunk-based development works with feature flags day to day](/blog/feature-flags-trunk-based-development/) . ## The relationship to CI/CD Trunk-based development is the branching half of continuous integration: small commits to a shared trunk keep integration conflicts tiny and constant rather than rare and enormous. Feature flags are the release half — they let a green, deployable trunk ship at any time while unfinished features stay hidden. Together they let "merge often" and "release when ready" coexist, which is the foundation most [progressive delivery](/glossary/progressive-delivery/) practices build on, and the reason [continuous delivery](/glossary/continuous-delivery/) can run on a trunk that always has unfinished work in it. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Dark launch A dark launch ships a feature to production turned off or invisible to users, so the code runs in the live environment before anyone sees it — you reveal it later on your own schedule.](/glossary/dark-launch/) [### Progressive delivery Progressive delivery is the practice of releasing software gradually and reversibly — using feature flags, canary releases, and percentage rollouts to expand exposure step by step while watching metrics, instead of shipping to everyone at once.](/glossary/progressive-delivery/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/glossary/variation/ # Variation — Definition [Home](/) / [Glossary](/glossary/) / Variation # Variation Also called: flag variation, feature flag variation Last updated: July 7, 2026 A **variation** is a value a [feature flag](/glossary/feature-flag/) can serve. A boolean flag has two variations, on and off; a multivariate flag defines several — strings, numbers, or JSON payloads — and each evaluation returns exactly one. The flag's rules, rollout, and default all resolve to a variation, and that value is what your code branches on. ## Boolean and multivariate variations The common case is a boolean flag with two variations, read through a helper like `boolVariation`. A [multivariate](/glossary/multivariate-testing/) flag has three or more — string, number, or JSON — which is what powers an [A/B test](/glossary/a-b-testing/) with several arms, tiered limits per plan, or a config payload delivered as a flag. Each variation has a stable key, and evaluation serves one of them to each user. ## How a variation is chosen [Evaluation](/glossary/flag-evaluation/) works through the flag in order — prerequisites, then targeting rules, then any [percentage rollout](/glossary/percentage-rollout/) , then the fallthrough default — and every path ends at exactly one variation, returned with a reason explaining the choice. [Sticky bucketing](/glossary/sticky-bucketing/) keeps a given user on the same variation across evaluations, so what they see stays stable as you ramp a rollout or run an experiment. Want the full picture? Read the concept guide: [What are feature flags? →](/docs/concepts/feature-flags/) ## Related terms [### Feature flag A feature flag is a conditional in your code whose value is controlled from a remote dashboard, letting you turn functionality on or off without deploying new code.](/glossary/feature-flag/) [### Multivariate testing Multivariate testing compares more than two variations at once — often combinations of several changes — to find which performs best, extending an A/B test beyond a single either/or split.](/glossary/multivariate-testing/) [### Flag evaluation Flag evaluation is the process an SDK runs to decide which variation a feature flag returns for a given user — applying prerequisites, targeting rules, and rollouts to the user context.](/glossary/flag-evaluation/) [Browse the full feature flag glossary →](/glossary/) ## Try it in your own app The Free plan covers 10 flags and 2 environments. No credit card, no demo call — sign up and ship. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/questions/ # Feature Flag Questions, Answered # Feature flag questions, answered Short answers to the things that go wrong with feature flags in practice. Each one names the causes in the order they actually occur, and points at the doc that proves it. [## Can I use one SDK key for every environment? No, and the key works nothing like a credential you could reuse anyway. It selects an environment: whichever one issued the key is the configuration your process receives. Point staging at the production key and staging reads production flag states, so a rollout you start in staging is a rollout in production.](/questions/can-i-use-one-sdk-key-for-every-environment/) [## Do I need a user ID to evaluate a feature flag? Not for on/off flags or targeting rules that match on other attributes. You need one for percentage rollouts, because the bucket comes from hashing the identifier with the flag key. Without it a server SDK evaluating locally serves the first (control) variation, while the hosted evaluation endpoint spreads anonymous traffic by weight with no per-user stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [## Do users lose a feature if I lower the rollout? Yes. Buckets are fixed per user, so lowering a rollout from 50% to 20% keeps everyone in buckets 0 through 19 and removes everyone from 20 through 49. It is deterministic, not a reshuffle, which is why raising the percentage again restores exactly the same people. Users who lose access see the feature disappear mid-session unless you evaluate once per session and cache the result.](/questions/do-users-lose-a-feature-if-i-lower-the-rollout/) [## Does changing a targeting rule re-bucket users? No. A user bucket comes from a salt stored on the flag and the user key, so editing rules, adjusting weights or adding conditions leaves every assignment untouched. Two things do reassign everybody: changing the attribute a rollout buckets by, and deleting a flag and recreating it, because a new flag gets a new salt.](/questions/does-changing-a-targeting-rule-re-bucket-users/) [## How do I test code that uses feature flags? Use the test client each SDK provides. You construct it with a map of flag keys to values and it evaluates against that map, with no network calls and no initialization step. It is not registered in the normal client cache, so every call returns an independent instance and tests cannot leak state into each other.](/questions/how-do-i-test-code-that-uses-feature-flags/) [## Is automated flag removal safe to let into my repo? The Action runs inside your own CI, so the only credential holding write access to your repository is the GITHUB_TOKEN that workflow already has. The Featureflip token you supply reads removal candidates and nothing else, and a Viewer token scoped to one project is enough. Every change arrives as a pull request you review, and dry-run mode prints each diff without contacting GitHub at all.](/questions/is-automated-flag-removal-safe-to-let-into-my-repo/) [## What if I archive a flag still used in my code? Archiving removes the flag from the configuration SDKs receive, so every remaining call site immediately falls back to the default value passed at that call site. Nothing throws and nothing logs an error, because the SDK cannot tell an archived flag from one that never existed. Remove the code first, then archive.](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) [## Why can't my browser SDK see a feature flag? The flag is not marked client-side visible. Client SDK keys are public by design, so they only ever receive flags you have explicitly opted in. A server key sees every flag in the project, which is why the same flag resolves from your backend and comes back empty in the browser. The client SDK reports it as not found rather than raising.](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) [## Why did my flag change take minutes to reach my app? Streaming delivers a flag change within seconds, so anything slower is not coming over the stream. When the stream cannot connect, most SDKs give up after five failed attempts and poll instead, every thirty seconds by default. A stream held open by a buffering proxy goes quiet without raising an error, so the SDK keeps waiting on a connection that delivers nothing.](/questions/why-did-my-flag-change-take-minutes-to-reach-my-app/) [## Why did my targeting rule not match? An earlier rule almost certainly matched first and stopped evaluation. Failing that, the attribute your rule tests never made it into the evaluation context, or the value is there but arrives as a different type or case than the rule expects. Rules run top to bottom and the first match wins, which makes rule order part of the logic.](/questions/why-did-my-targeting-rule-not-match/) [## Why didn't the cleanup Action remove my flag? Almost always because your code reads the flag through your own wrapper function instead of calling the SDK directly. Matching is by method name plus key string, so a wrapper is invisible to the rules and the run reports no changes. The other common causes are a refusal it prints explicitly, such as a mock stub holding the flag read, or a key that is not a string literal at the read site.](/questions/why-didnt-the-cleanup-action-remove-my-flag/) [## Why do teammates get different variations? A rollout buckets by user key by default, and each user hashes independently. Two colleagues in the same workspace count as two separate users, so a 50% rollout splits them roughly half the time. Point the rollout at an account or organization attribute instead, and everyone sharing that value moves together.](/questions/why-do-users-on-the-same-team-get-different-variations/) [## Why do two SDKs return different variations? One of them is almost certainly evaluating a keyless context. Bucketing needs a stable user key, and the two evaluation paths handle its absence differently on purpose. A server SDK evaluating locally serves the first variation, while the hosted endpoint assigns a fresh random bucket per call. Pass the same user key everywhere and the two agree, since every SDK runs the same hash.](/questions/why-do-two-sdks-return-different-variations/) [## Why do two users get different variations? In a percentage rollout that is the expected behavior. Each user is hashed together with the flag key to produce a bucket from 0 to 100, so a 30% rollout puts roughly three users in ten on the new variation. The same user always lands in the same bucket for the same flag. Hashing is per flag, so being inside the 30% for one tells you nothing about any other.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [## Why does my flag flash the wrong value on load? Your UI is rendering inside the window before the browser SDK has its first flag payload, and during that window every call returns the default value you passed. The reason string reads FlagNotFound rather than an error, since the flag is simply absent from an empty snapshot. Wait for initialization before rendering the gated region, or render a neutral state until the client is ready.](/questions/why-does-my-flag-flash-the-wrong-value-on-page-load/) [## Why does my flag work in dev but not production? Every environment holds its own configuration, so enabling a flag in development changes nothing in production. Targeting rules and rollout percentages are per environment as well, which means a flag can read as on in both and still serve different values. A new environment starts every existing flag in the off state deliberately, so nothing goes live by accident.](/questions/why-does-my-flag-work-in-development-but-not-production/) [## Why does my flag work locally but not in Docker? The SDK key almost certainly did not reach the container. A key read from a local env file that is never passed through in your compose file or Dockerfile arrives empty, and the SDK serves your default value rather than failing loudly. After that, suspect outbound network. A container on an internal-only network cannot reach the evaluation endpoint, so the first payload never arrives.](/questions/why-does-my-feature-flag-work-locally-but-not-in-docker/) [## Why doesn't my rollout hit the percentage I set? Bucketing assigns each user a fixed number from 0 to 99 and counts how many land under your threshold, rather than metering traffic. With a few hundred users the observed share can sit several points either side of the configured one, and that is expected. Persistent skew usually means anonymous contexts, which the hosted endpoint spreads randomly on every call instead of bucketing.](/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/) [## Why doesn't my segment match any users? The rule probably cannot resolve the segment at all, which fails closed and matches nobody. A client SDK never given segment data behaves the same way. Once that is ruled out, look at condition logic. Segments carry one flat condition list with a single AND or OR, so a segment built to catch two different user groups with AND matches nobody by construction.](/questions/why-doesnt-my-segment-match-any-users/) [## Why doesn't my semver targeting rule match? The value you are sending does not parse as a semantic version. An unparseable string matches nothing at all, with no fallback to text comparison, which is why the rule looks dead instead of behaving oddly. A leading v is fine and 2.0 compares equal to 2.0.0, but a build number like 1042 or a date will never satisfy a semver condition.](/questions/why-doesnt-my-semver-targeting-rule-match/) [## Why is my feature flag returning the default value? The SDK almost certainly cannot find the flag at all, so evaluation never got far enough to fail. Check the SDK key first, since it may belong to a different environment, then whether the flag is simply off there. After that comes a misspelled key, a client still initializing, a type mismatch against the variation, or a flag that is not client-side visible when you ask from a browser.](/questions/why-is-my-feature-flag-returning-the-default-value/) [## Why is my feature flag returning the wrong type? The SDKs do not coerce between types. If the variation holds a string and you call the boolean method, you get the default you passed and no conversion happens. The dashboard then shows the flag serving one thing while your code reports another, which reads like a stale cache and is really a type mismatch.](/questions/why-is-my-feature-flag-returning-the-wrong-type/) [## Why is my flag evaluation returning an error? The evaluator picked a variation key the flag no longer defines, which normally means a rule or fallthrough still references a deleted variation. The SDK serves the default value you passed and reports Error, declining to guess which survivor you meant. A variation that exists but holds a null value falls outside this, so the reason string reliably separates a config problem from a data one.](/questions/why-is-my-flag-evaluation-returning-an-error/) ## Looking for something longer? The [documentation](/docs/) covers every SDK in depth, the [glossary](/glossary/) defines the vocabulary, and the [blog](/blog/) works through full patterns end to end. ## Try it yourself The Free plan covers 10 flags and 2 environments, which is enough to reproduce everything on this page. [Get started free](https://app.featureflip.io/signup) [See pricing](/pricing/) --- ## Source: https://featureflip.io/questions/can-i-use-one-sdk-key-for-every-environment/ # Can I use one SDK key for every environment? [Home](/) / [Questions](/questions/) # Can I use one SDK key for every environment? Last updated: August 3, 2026 No, and the key works nothing like a credential you could reuse anyway. It selects an environment: whichever one issued the key is the configuration your process receives. Point staging at the production key and staging reads production flag states, so a rollout you start in staging is a rollout in production. The temptation is understandable. A shared key cuts down on config sprawl, and the thing reads like an authentication token, where one token per service sounds tidier than four. Authentication is the wrong mental model. The key answers a different question entirely, which is whose configuration this process should receive. Every environment issues its own, and that is the whole mechanism by which a flag can be on in development and off in production. ## What sharing one actually does Give staging the production key and staging is now reading production. Toggle a flag to test something and you have toggled it for real users. The dashboard will show exactly what you did, in the production environment, because that is where the change landed. The reverse fails more quietly. A production deploy carrying a development key reads development’s configuration, and every flag you enabled locally months ago is suddenly live. ## The shape that works One key per environment, supplied the same way you supply a database URL: .env.development ```bash FEATUREFLIP_SDK_KEY=sdk_server_dev_a1b2c3d4 # production secrets FEATUREFLIP_SDK_KEY=sdk_server_prod_z9y8x7w6 ``` Keep them out of source control. They belong in environment variables or a secrets manager, alongside everything else that differs per deploy. ## Server and client keys are also separate Within a single environment there are two kinds. A server key stays on your infrastructure and can see every flag in the project, while a client key ships to browsers and only ever receives the flags you have marked client-side visible. They are not interchangeable, and the prefix tells you which one you are holding: ```plaintext sdk_server_... backend only, sees everything sdk_client_... safe to ship, sees opted-in flags only ``` Shipping a server key to a browser is the mistake with real consequences, because it exposes every flag and every targeting rule in the project. ## If a key leaks Regenerate it from the dashboard. Only that environment is affected, which is the point of keeping them separate, and you update one deploy config rather than all of them. Both key types, and how the isolation between environments is enforced, are written up in [Environments](/docs/concepts/environments/) . ## Related questions [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [### Why can't my browser SDK see a feature flag? A flag has to be marked client-side visible before a client SDK key can receive it. Server keys see every flag, client keys see only the opted-in ones.](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/ # Do I need a user ID to evaluate a feature flag? [Home](/) / [Questions](/questions/) # Do I need a user ID to evaluate a feature flag? Last updated: August 3, 2026 Not for on/off flags or targeting rules that match on other attributes. You need one for percentage rollouts, because the bucket comes from hashing the identifier with the flag key. Without it a server SDK evaluating locally serves the first (control) variation, while the hosted evaluation endpoint spreads anonymous traffic by weight with no per-user stickiness. A flag that is simply on or off needs no context at all, and neither does a targeting rule keyed on something else, like a plan tier or a region. Where the identifier earns its keep is in deciding which side of a percentage split a given caller lands on. ## Why the identifier is what makes a rollout stick Bucketing hashes the user identifier together with the flag key, and the result is a number from 0 to 100. Feed it the same inputs and you get the same number every time, which is what stops a user flipping between variations on refresh, across devices, or between API calls. Take the identifier away and nothing stable is left to hash. ## What actually happens without one This is where the two evaluation paths stop agreeing, and the difference catches people out. **A server SDK evaluating locally serves the first variation**, the control. It does not randomize. An anonymous caller gets the safe side of the split deterministically, which is the conservative choice for a backend that may be handling a logged-out request. **The hosted evaluation endpoint spreads anonymous traffic across variations by weight.** The overall split comes out roughly right, but no individual caller is sticky, so the same visitor can see different variations on consecutive requests. Both are deliberate. They answer the same question about what to do when stickiness is impossible, and each answer suits the place it runs. ## What to do for logged-out users Generate a stable key yourself and pass it as the identifier. A UUID in a cookie or in local storage is enough: ```js let id = localStorage.getItem('ff-anon-id'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('ff-anon-id', id); } const client = FeatureflipClient.get({ clientKey: 'sdk_client_a1b2c3d4', context: { user_id: id }, }); ``` Now the anonymous visitor buckets like any other user and stays put. Swap the generated key for the real user ID once they sign in, and be aware that the bucket may change at that moment, because the hash input changed. For a checkout experiment that is usually fine. For anything where crossing the boundary mid-session would be jarring, hold the anonymous key until the session ends. ## How much anonymity you keep is your call Supplying a key buys stickiness back at any point, so the decision is about how much you want to track rather than about a limit in the flag system. The anonymous case is written up alongside the rest of the bucketing mechanics in [Rollout strategies](/docs/concepts/rollout-strategies/) . ## Related questions [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### How do I test code that uses feature flags? Every Featureflip SDK ships a test client you construct with fixed flag values. No network, no initialization, no mocking the SDK surface yourself.](/questions/how-do-i-test-code-that-uses-feature-flags/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/do-users-lose-a-feature-if-i-lower-the-rollout/ # Do users lose a feature if I lower the rollout? [Home](/) / [Questions](/questions/) # Do users lose a feature if I lower the rollout? Last updated: August 6, 2026 Yes. Buckets are fixed per user, so lowering a rollout from 50% to 20% keeps everyone in buckets 0 through 19 and removes everyone from 20 through 49. It is deterministic, not a reshuffle, which is why raising the percentage again restores exactly the same people. Users who lose access see the feature disappear mid-session unless you evaluate once per session and cache the result. Yes, and you can say in advance exactly which users, which is the useful part. A user’s bucket is a fixed number from 0 to 99 derived from a per-flag salt and their user key, as [Rollout strategies](/docs/concepts/rollout-strategies/) sets out. It does not change when you edit the rollout. What changes is where the boundary sits. At 50% the enabled band covers buckets 0 through 49, and dropping to 20% narrows it to 0 through 19. Everyone from 20 to 49 loses the feature, and everyone below 20 never notices. ## Why the ladder is safe Because the assignment is stable, the ladder up and back down is symmetric. Going 5% to 25% to 50% only ever adds people. Nobody who had the feature at 5% loses it at 25%, so your early cohort keeps a consistent experience through the whole rollout and any metrics you gather on them stay comparable. Roll back to 5% and you get the same users you started with, down to the individual. Which is why a percentage gate is safe to reach for mid-incident. ## What users actually see The change reaches connected SDKs within seconds. That speed is the whole point of a percentage gate, and it also means a user can lose a feature while they are looking straight at it, which is the one part of this worth designing around. ```js // Evaluate once at session start, then reuse the answer const canSeeNewCheckout = client.boolVariation('new-checkout', ctx, false); ``` For anything a user is midway through, evaluate at a natural boundary and hold the result for that session instead of re-evaluating on every render. A checkout that vanishes between steps earns you a support ticket, where one that survives until the next page load generally goes unnoticed. In a genuine emergency the immediate cut is of course exactly what you want. ## One thing that does re-bucket Changing the rollout percentage is safe. Changing which attribute the rollout buckets by replaces one of the hash inputs, so it reassigns everybody at once. Treat that as starting a new rollout, and see [Progressive rollouts](/use-cases/progressive-rollouts/) for what to watch at each step of one. Deleting and recreating a flag has the same effect, since a fresh flag gets a fresh salt. ## Related questions [### Why doesn't my rollout hit the percentage I set? A 20% rollout rarely serves exactly 20% of traffic. Bucketing is deterministic per user, so small populations skew and anonymous traffic behaves differently.](/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/) [### Does changing a targeting rule re-bucket users? Editing rules, weights or conditions leaves every bucket where it was. Only two changes reassign users, and both are easy to make by accident.](/questions/does-changing-a-targeting-rule-re-bucket-users/) [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/does-changing-a-targeting-rule-re-bucket-users/ # Does changing a targeting rule re-bucket users? [Home](/) / [Questions](/questions/) # Does changing a targeting rule re-bucket users? Last updated: August 6, 2026 No. A user bucket comes from a salt stored on the flag and the user key, so editing rules, adjusting weights or adding conditions leaves every assignment untouched. Two things do reassign everybody: changing the attribute a rollout buckets by, and deleting a flag and recreating it, because a new flag gets a new salt. No. Ordinary targeting edits do not move anybody. The bucket is computed from two inputs: a salt that lives on the flag from the moment it is created, and the key identifying the user. Neither is affected by editing a rule, reordering rules, adjusting weights, or adding conditions. A user in bucket 37 stays in bucket 37 for the life of the flag. What your edits change is which band bucket 37 falls into, and which rule gets to answer before the rollout is consulted at all. ## Where the confusion comes from Changing variation and being re-bucketed are two different events, and they are easy to conflate because they look identical to the user. Adding a rule above a rollout pulls matching users out of the rollout entirely, because rules run in priority order and the first match wins. Those users now get the rule’s variation. Their bucket did not move. Remove the rule and they fall through to exactly the band they were always in. Widening a rollout works the same way. The people who gain the feature at 50% are the ones whose fixed buckets sit between the old boundary and the new one. ## The two changes that do reassign Changing `bucketBy` replaces one of the two hash inputs, so every user is recomputed at once. Switching from a user key to an account key is a legitimate thing to want, but treat it as starting a new rollout rather than tuning an existing one. Anyone mid-experiment will be redistributed. Deleting a flag and recreating it under the same key does the same thing less visibly. The new flag carries a new salt, so the population reshuffles even though the key, variations and rules look identical to what you had. If you need a clean reassignment, this is the way to get one deliberately. ## Practical consequence You can iterate on targeting during a live rollout without disturbing the cohort you are measuring. Add an internal-staff rule, fix a condition, reorder your rules, all without touching who is in the experiment. The one thing to leave alone until it finishes is `bucketBy`. [Rollout strategies](/docs/concepts/rollout-strategies/) walks through the salt and the key that feed the hash. If what you are really chasing is why a user’s variation changed, rule precedence in [Targeting and segments](/docs/concepts/targeting/) is the more likely explanation. ## Related questions [### Do users lose a feature if I lower the rollout? Yes, and which users lose it is predictable rather than random. Lowering a rollout removes the top of the bucket range, so the earliest users keep the feature.](/questions/do-users-lose-a-feature-if-i-lower-the-rollout/) [### Why do teammates get different variations? A percentage rollout buckets by individual user, so colleagues in one workspace land on different sides of it. Bucket by account to keep them together.](/questions/why-do-users-on-the-same-team-get-different-variations/) [### Why doesn't my rollout hit the percentage I set? A 20% rollout rarely serves exactly 20% of traffic. Bucketing is deterministic per user, so small populations skew and anonymous traffic behaves differently.](/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/how-do-i-test-code-that-uses-feature-flags/ # How do I test code that uses feature flags? [Home](/) / [Questions](/questions/) # How do I test code that uses feature flags? Last updated: August 3, 2026 Use the test client each SDK provides. You construct it with a map of flag keys to values and it evaluates against that map, with no network calls and no initialization step. It is not registered in the normal client cache, so every call returns an independent instance and tests cannot leak state into each other. Mocking the SDK yourself works right up until the surface changes underneath you. Every Featureflip SDK ships a purpose-built test client instead, so you assert on your own branching rather than on a stub you maintain. ## The shape Pass in the flag values your test needs, and that is the setup done. ```js import { FeatureflipClient } from '@featureflip/node'; const client = FeatureflipClient.forTesting({ 'new-checkout': true, 'checkout-copy': 'variant-b', }); client.boolVariation('new-checkout', false); // -> true ``` true"> Python and Java are the same idea: ```python client = FeatureflipClient.for_testing({ "new-checkout": True, }) ``` ```java FeatureflipClient client = FeatureflipClient.forTesting(Map.of( "new-checkout", true )); ``` Go exposes a package-level `ForTesting()` that behaves the same way. The exact constructor name varies by language, so check the [SDK reference](/docs/sdks/overview/) for yours. ## Why it is not the normal client It performs no network calls and needs no initialization, so tests stay fast and run offline. It also sits outside the client cache the SDKs use to guarantee one shared instance per process, which means each call hands back an independent client. Two tests configuring the same key to different values cannot interfere. That second point is the one that bites if you roll your own. The production client is deliberately shared, and a hand-built singleton mock will happily leak the last test’s flag values into the next. ## Test both sides The point of a flag is that two paths exist. Cover only the enabled path and you have tested half of what you shipped, usually not the half most of your users are running. ```js for (const enabled of [true, false]) { const client = FeatureflipClient.forTesting({ 'new-checkout': enabled }); // assert the behavior you expect on each side } ``` Keep the default you pass at the call site in mind too. It is what runs during an outage, so it deserves a test of its own rather than being treated as unreachable. ## What this does not cover Targeting rules and rollout percentages live in the flag configuration, not in your code. The test client returns the value you gave it, so it verifies your branching and says nothing about whether a rule is correct. Rule behavior belongs in a staging environment against real configuration. ## Related questions [### Do I need a user ID to evaluate a feature flag? Only for percentage rollouts. Without a stable identifier a server SDK serves the control variation, and the hosted endpoint spreads traffic with no stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [### Why is my feature flag returning the wrong type? SDKs do not coerce types. Asking for a boolean from a string flag returns the default you passed rather than converting, so code and dashboard disagree.](/questions/why-is-my-feature-flag-returning-the-wrong-type/) [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/is-automated-flag-removal-safe-to-let-into-my-repo/ # Is automated flag removal safe to let into my repo? [Home](/) / [Questions](/questions/) # Is automated flag removal safe to let into my repo? Last updated: September 17, 2026 The Action runs inside your own CI, so the only credential holding write access to your repository is the GITHUB_TOKEN that workflow already has. The Featureflip token you supply reads removal candidates and nothing else, and a Viewer token scoped to one project is enough. Every change arrives as a pull request you review, and dry-run mode prints each diff without contacting GitHub at all. Something is about to open pull requests against your source. Start with the credential. ## Which credential can write to your repo The Featureflip cleanup Action is a step in your own workflow. It runs on your runner, against a checkout you made, and the thing that writes to your repository is the `GITHUB_TOKEN` that workflow already carries: ```yaml permissions: contents: write # push the removal branch pull-requests: write # open the PR ``` Two scopes. That is the whole ask. No app gets installed on the repository, and the parsing and rewriting both happen inside the container on your runner, so your source never leaves it. The Featureflip token you pass in is the other credential, and it points the other way. In `remove` mode the Action only reads from the API, so a `Viewer` token restricted to one project covers it. That token cannot archive, toggle or delete a flag. Retiring the flag once its removal lands stays a decision you make, and the separate workflow that automates it carries [its own write-scoped secret](/docs/integrations/flag-cleanup-action/) . ## Nothing merges itself Each flag gets one pull request. Dead flags open ready for review, stale flags open as drafts, and both then wait for whatever reviewers and status checks your repository already requires before anything can land on your default branch. There is a cap on how many pull requests one run may open. Set it low the first time: ```yaml with: max-prs: 3 ``` If something about a run is wrong, that bounds how many pull requests are wrong before a person sees one. Nothing is lost by capping it, because the next run picks up the flags this one did not reach. ## What a run refuses to touch The rewriter carries a fixed list of shapes it will not fold, and it says so when it hits one. Hit a mock stub holding a flag read and the flag stops, non-zero, file named. Keys that are not string literals at the read site produce no diff. Your own wrapper function around the SDK is left alone, which is far and away the most common reason [a run reports no changes](/questions/why-didnt-the-cleanup-action-remove-my-flag/) for a flag you can grep for in forty files. None of those produce a partial rewrite. Each is printed with a label, and the reasoning behind every entry on that list is [the specification for the whole tool](/blog/automated-feature-flag-removal/) . ## Watching a whole run before it touches anything Set `dry-run: true` and the Action computes and prints the diff for every candidate while never contacting GitHub, not even to check whether a pull request already exists for a flag: ```yaml with: dry-run: true flags: checkout-v2 ``` Every refusal a real run can produce is reachable in a dry run and is labeled identically, so the preview cannot approve a flag that the next real run turns down. Adding `flags` narrows the whole thing to one key. Those are the same rules a real run applies to your real source, with nothing written down anywhere, so you can read a whole run through before deciding whether this workflow gets the two scopes above. ## Related questions [### Why didn't the cleanup Action remove my flag? A cleanup run that reports no changes for a flag your code reads everywhere is almost always a wrapper function. The other causes, and how to tell them apart.](/questions/why-didnt-the-cleanup-action-remove-my-flag/) [### What if I archive a flag still used in my code? An archived flag leaves the SDK configuration, so any remaining call site falls back to the default you passed. Remove the code first, then archive.](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/what-happens-when-i-archive-a-flag-still-in-my-code/ # What if I archive a flag still used in my code? [Home](/) / [Questions](/questions/) # What if I archive a flag still used in my code? Last updated: August 3, 2026 Archiving removes the flag from the configuration SDKs receive, so every remaining call site immediately falls back to the default value passed at that call site. Nothing throws and nothing logs an error, because the SDK cannot tell an archived flag from one that never existed. Remove the code first, then archive. Archiving is a configuration change with a runtime effect, which is easy to miss because the word sounds like filing something away. The configuration served to SDKs excludes archived flags. Within seconds of archiving, connected clients hold a snapshot that no longer contains the key, and every call site for it starts returning its default. ## Why this is silent An SDK has no way to distinguish an archived flag from a typo. Either way the key is absent from the snapshot, so it reports `flag-not-found` and hands back your default instead of raising, which is the fallback rule described in [Reliability and resilience](/docs/concepts/reliability/) . The same safety property that keeps your app running through an outage is what hides this from you. The failure is behavioral rather than an exception, which means whatever your default happens to be is now what every user gets. ## Which means the default matters ```js // Archived flag, code still present client.boolVariation('new-checkout', false); // -> false, feature off ``` false, feature off"> If the flag was fully rolled out and the code still passes `false` as the default, archiving it turns the feature off for everyone. Your rollout said 100% and the archive quietly overruled it, without anything in between raising a complaint. ## The order that works - Roll the flag to 100% and let it sit until you trust it. - Delete the flag from the code, keeping the winning branch. - Deploy and confirm nothing references the key. - Archive the flag. Step 3 is the one people skip. Grep for the key across the repository, including tests, config and any other service that shares the project. ## Featureflip will not archive anything for you Stale and dead flag detection is a signal and nothing more. Nothing archives, disables or deletes a flag automatically, precisely because the runtime consequence above is real and the decision needs a person who knows whether the code is gone. [Stale flag detection](/docs/concepts/stale-flags/) explains what the signal actually measures and why it stops at telling you. ## If you archived too early Restore it. The flag returns to the configuration with its settings intact and call sites resume evaluating normally. Fix the ordering, then archive again. ## Related questions [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### How do I test code that uses feature flags? Every Featureflip SDK ships a test client you construct with fixed flag values. No network, no initialization, no mocking the SDK surface yourself.](/questions/how-do-i-test-code-that-uses-feature-flags/) [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-cant-my-browser-sdk-see-a-feature-flag/ # Why can't my browser SDK see a feature flag? [Home](/) / [Questions](/questions/) # Why can't my browser SDK see a feature flag? Last updated: August 3, 2026 The flag is not marked client-side visible. Client SDK keys are public by design, so they only ever receive flags you have explicitly opted in. A server key sees every flag in the project, which is why the same flag resolves from your backend and comes back empty in the browser. The client SDK reports it as not found rather than raising. A client SDK key ships to the browser, where anyone can read it. Everything about the design follows from that one constraint, because a client key must never be able to pull down flags you did not intend to publish, along with whatever targeting rules are attached to them. So visibility is opt-in per flag. Until you tick **Client-side visible**, a client key receives nothing for that key, and the SDK synthesizes a `flag-not-found` locally because the flag is simply absent from its snapshot. ## How to tell this is what you have The signature is a flag that works from one runtime and not the other, with no error in either. ```js // Backend, server key: works client.boolVariation('new-checkout', false); // -> true // Browser, client key: returns the default client.boolVariation('new-checkout', false); // -> false ``` true// Browser, client key: returns the defaultclient.boolVariation('new-checkout', false); // -> false"> If the reason string is available to you, `flag-not-found` from a client SDK narrows things down usefully. The key might not exist, the client might still be initializing, or the flag is not client-side visible. Rule out the first two and you have your answer. The [analytics integration guide](/docs/guides/analytics-integration/) carries the full reason table. ## The fix Open the flag in the dashboard and enable client-side visibility. The change reaches connected clients over the existing stream within seconds. No redeploy. If you are unsure which kind of key your process is holding, [Environments](/docs/concepts/environments/) covers how the two divide. ## Why this is worth keeping on It is tempting to mark everything visible and move on. Consider what that publishes. Targeting rules travel with a flag, so a rule that serves a feature to `plan == "enterprise"` tells anyone reading your bundle that an enterprise plan exists and what unlocks it. Internal kill switches and pricing gates are the usual regrets. If a flag would embarrass you in a public repository, keep it server-side. ## Naming helps Prefixing client-visible flags makes the boundary visible in code review. A key like `web-new-checkout` reads as public at the call site, which is exactly where someone is deciding whether to add a targeting rule to it. ## Related questions [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### Can I use one SDK key for every environment? No. The SDK key is what selects the environment, so sharing one collapses dev and production onto the same flag configuration.](/questions/can-i-use-one-sdk-key-for-every-environment/) [### Do I need a user ID to evaluate a feature flag? Only for percentage rollouts. Without a stable identifier a server SDK serves the control variation, and the hosted endpoint spreads traffic with no stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-did-my-flag-change-take-minutes-to-reach-my-app/ # Why did my flag change take minutes to reach my app? [Home](/) / [Questions](/questions/) # Why did my flag change take minutes to reach my app? Last updated: September 14, 2026 Streaming delivers a flag change within seconds, so anything slower is not coming over the stream. When the stream cannot connect, most SDKs give up after five failed attempts and poll instead, every thirty seconds by default. A stream held open by a buffering proxy goes quiet without raising an error, so the SDK keeps waiting on a connection that delivers nothing. A flag change should reach a connected SDK in seconds. When it takes longer, the shape of the delay tells you what happened. Up to thirty seconds of lag, every time, means the SDK is picking changes up on its default poll interval instead of the stream. If the change only ever lands when the process restarts, nothing is refreshing at all. ## Why the stream drops When the stream cannot connect, the SDK [retries on its own](/docs/api/streaming/) and waits a little longer after each failure, up to thirty seconds. Most SDKs stop after five failures in a row and [switch to polling](/docs/concepts/reliability/) , which is where the steady half-minute lag comes from. Node logs it: `SSE connection failed after 5 retries, falling back to polling`. Only outright failures count. A connection that gets its first snapshot through resets the tally, so an HTTP client that applies its overall timeout to a long-lived stream causes churn and never reaches the fallback. Set it to abort any request after ten seconds and it tears the stream down on schedule, with the SDK reconnecting and picking up a full snapshot each time. We shipped exactly this bug in our own C# SDK once. Reverse proxies and some corporate egress paths hold a response until it completes, and a stream never completes, so nothing gets through, not even the ping the server sends every thirty seconds. No error reaches the SDK either. The C#, Java, Python and Ruby SDKs close a stream after ninety seconds of silence and try again. ## Confirming which one you have Turn streaming off and watch the next change. ```js import { FeatureflipClient } from '@featureflip/node'; // Streaming on (the default): changes arrive in seconds const client = FeatureflipClient.get({ sdkKey, streaming: true }); // Streaming off: changes arrive on the poll interval instead const client = FeatureflipClient.get({ sdkKey, streaming: false, pollInterval: 30_000 }); ``` If it lands within one poll interval, your key and the service are both fine, and the stream is the only thing left that can be failing. A healthy stream is the SDK’s only update path. The polling fallback waits for a stream that keeps failing, and one that connects and then carries nothing never fails in that sense, so the SDK keeps serving the last snapshot it received. ## The fix Give the streaming connection its own timeout settings instead of letting it inherit your general HTTP client’s, and make sure nothing in the path buffers responses. If the network you run in will not hold a stream open, turn streaming off and pick a poll interval you can design around. Restart the process after either change so the SDK starts on a fresh connection. ## Related questions [### Why does my flag flash the wrong value on load? A client SDK serves your default until its first payload arrives. The flash is that gap becoming visible, and rendering around it closes the gap for good.](/questions/why-does-my-flag-flash-the-wrong-value-on-page-load/) [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-did-my-targeting-rule-not-match/ # Why did my targeting rule not match? [Home](/) / [Questions](/questions/) # Why did my targeting rule not match? Last updated: August 3, 2026 An earlier rule almost certainly matched first and stopped evaluation. Failing that, the attribute your rule tests never made it into the evaluation context, or the value is there but arrives as a different type or case than the rule expects. Rules run top to bottom and the first match wins, which makes rule order part of the logic. A rule matches on an attribute, an operator, and a set of values. Any of those can be quietly wrong, and from the call site all three failures look the same. ## 1. An earlier rule already matched Rules are evaluated in order and the first match wins. Evaluation stops there. So a broad rule sitting above a narrow one makes the narrow one unreachable: ```plaintext 1. plan equals "pro" -> serve true 2. email equals "you@co.com" -> serve false ``` serve true2. email equals "you@co.com" -> serve false"> If you are on the pro plan, rule 2 never runs. There is nothing wrong with it. It simply never gets reached. Order rules from most specific to most general. Individual users first, then segments, then broad attributes like plan or region. This is also why adding a new rule at the bottom of a long list often appears to do nothing. ## 2. The attribute is not in the context Rules can only test what you send. An attribute you never included is absent, and a rule on it cannot match: ```js // Rule tests `plan`, context does not carry it client.boolVariation('new-checkout', { user_id: 'u_123' }, false); // Rule can now match client.boolVariation('new-checkout', { user_id: 'u_123', plan: 'enterprise' }, false); ``` Context is supplied per call rather than stored, so this can differ between two call sites in the same app. A flag that works on one endpoint and not another is usually this. One useful consequence of that is upgrades landing immediately. Nothing is cached against the user, so when someone moves to a new plan, targeting picks it up on the very next request that carries the new value. ## 3. The value does not compare the way you expect Comparisons are exact. `"Enterprise"` does not equal `"enterprise"`, and the string `"true"` is not the boolean `true`. Numeric IDs sent as numbers will not match rule values written as strings. Pick the operator deliberately too. `equals`, `contains`, `startsWith`, `endsWith` and `in` are all available, and `contains` on an email domain behaves differently from `endsWith` in exactly the case you care about. A rule matching `contains "@acme.com"` also matches `user@acme.com.attacker.net`. ## Reading the outcome The reason string names what happened. `rule-match:{id}` tells you which rule won, so you can compare it against the one you expected. `fallthrough` means no rule matched at all, which points at cause 2 or 3 rather than ordering. For the full operator list and how segments fit in, see [User targeting and segments](/docs/concepts/targeting/) . The [targeting rules guide](/docs/guides/targeting-rules/) works through the ordering problem above with real rules. ## Related questions [### Do I need a user ID to evaluate a feature flag? Only for percentage rollouts. Without a stable identifier a server SDK serves the control variation, and the hosted endpoint spreads traffic with no stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-didnt-the-cleanup-action-remove-my-flag/ # Why didn't the cleanup Action remove my flag? [Home](/) / [Questions](/questions/) # Why didn't the cleanup Action remove my flag? Last updated: September 4, 2026 Almost always because your code reads the flag through your own wrapper function instead of calling the SDK directly. Matching is by method name plus key string, so a wrapper is invisible to the rules and the run reports no changes. The other common causes are a refusal it prints explicitly, such as a mock stub holding the flag read, or a key that is not a string literal at the read site. A run that finishes cleanly and reports `no-changes` for a flag you can grep for in forty files looks like a bug. It usually is not. There are three different reasons a flag survives a cleanup run, and they are easy to tell apart once you know the run prints all of them. ## The wrapper case, which is most of them The rules match a flag read by callee name plus key string. A direct SDK call is recognized: ```ts if (client.boolVariation('checkout-v2', ctx, false)) { ``` Your own helper is not: ```ts if (useFlag('checkout-v2')) { ``` Nothing about `useFlag` says it is a flag read. It could add logging, supply a default, or read from somewhere else entirely, and rewriting an unknown function because its first argument happened to be a flag key is how a tool deletes a real call. So it does nothing, and reports `no-changes`. Most production codebases wrap the SDK, so this is the ordinary outcome and not an edge case. You can call the SDK directly at the read site, or add the key to `ignore` so it stops appearing in the queue. If your wrapper is stable and widely used, name it in `accessors` and the discarded-read handling will cover it. ## When the run refuses outright Some shapes are not skipped quietly. They stop the flag and exit non-zero, naming the file: ```java when(client.boolVariation("checkout-v2", ctx, false)).thenReturn(true); ``` Folding that read produces `when(true).thenReturn(true)`, which compiles while quietly no longer stubbing anything. Mockito, MockK, ts-mockito, `package:mockito` and gomock expectations are all refused for the same reason. A Python read bound at module level is refused too, because the name is public API and the importing modules are never in the candidate set. Look for `[unsafe-rewrite]` or `[piranha-error]` in the output. A refusal is always printed, so a flag that fails this way is never a silent one. The reasoning behind each refusal is covered in [what an automated flag removal has to decline](/blog/automated-feature-flag-removal/) . ## When the key is not a literal If the key is not a string literal at the read site, the rules cannot tell which flag the call belongs to: ```ts const on = client.boolVariation(keyFromConfig, ctx, false); ``` The one exception is a constant declared in the same file, which is resolved and removed with the read. A constant in a different file is out of reach, because the tool parses one file at a time. ## Confirming which one you hit Run with `dry-run: true`. Every refusal a real run can produce is reachable in a dry run and reported identically, so the preview tells you whether you are looking at a quiet `no-changes` or a printed refusal, without opening a pull request. The two outcomes have different fixes, and the output names which one you have. ## Related questions [### Is automated flag removal safe to let into my repo? What holds write access during an automated flag cleanup run, what the run declines to touch, and how to watch a full run before it opens anything.](/questions/is-automated-flag-removal-safe-to-let-into-my-repo/) [### What if I archive a flag still used in my code? An archived flag leaves the SDK configuration, so any remaining call site falls back to the default you passed. Remove the code first, then archive.](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) [### How do I test code that uses feature flags? Every Featureflip SDK ships a test client you construct with fixed flag values. No network, no initialization, no mocking the SDK surface yourself.](/questions/how-do-i-test-code-that-uses-feature-flags/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-do-two-sdks-return-different-variations/ # Why do two SDKs return different variations? [Home](/) / [Questions](/questions/) # Why do two SDKs return different variations? Last updated: August 6, 2026 One of them is almost certainly evaluating a keyless context. Bucketing needs a stable user key, and the two evaluation paths handle its absence differently on purpose. A server SDK evaluating locally serves the first variation, while the hosted endpoint assigns a fresh random bucket per call. Pass the same user key everywhere and the two agree, since every SDK runs the same hash. Your Go service says `true` and your Node service says `false` for the same user on the same flag. Before suspecting either SDK, check what you are passing as the user key, because that is the cause in most reports of this. Every SDK computes the same bucket from the same inputs. The hash takes a per-flag salt and the user’s key, and the result is a number from 0 to 99 that decides which weighted band the user falls into. Same salt and same key produce the same number in every language we ship. That parity is covered by a shared set of golden vectors run against each SDK, so drift between implementations gets caught in CI rather than in your logs. ## What actually differs Everything hinges on what each path does when the key is missing, and both behaviors are deliberate. ```js // Both services pass the key: identical result everywhere boolVariation('new-pricing', { userId: 'u_8134' }, false); // One service omits it: results diverge by evaluation path boolVariation('new-pricing', {}, false); ``` A server-side SDK evaluating locally serves the first variation, deterministically, every time. The hosted evaluation endpoint instead assigns a random bucket per call so anonymous traffic spreads across variations by weight. Both behaviors are correct for where they run, and they answer slightly different questions, so a service that quietly drops the user key ends up disagreeing with one that keeps it. Attribute naming is the second thing to check. The built-in identifier is accepted as either `userId` or `user_id` and the two are treated as the same field, but a custom attribute is matched literally. A context sending `accountId` where your rule expects `account_id` quietly fails the condition and raises nothing. ## Rule out the config first The two services must also be reading the same configuration. Different SDK keys mean different environments, and a flag at 50% in staging and 10% in production will disagree for perfectly good reasons. Confirm both keys belong to the environment you think they do before debugging the evaluation itself. ## The fix Send the same stable key from every service that evaluates the flag, and use the same attribute names in each. Where a request genuinely has no user, pick your path knowingly. It determines whether you get the anonymous spread or the control variation. [Rollout strategies](/docs/concepts/rollout-strategies/) sets out both the keyed and keyless behaviors side by side. If the SDK keys turn out to be the problem rather than the context, start from [Environments](/docs/concepts/environments/) . ## Related questions [### Why doesn't my rollout hit the percentage I set? A 20% rollout rarely serves exactly 20% of traffic. Bucketing is deterministic per user, so small populations skew and anonymous traffic behaves differently.](/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/) [### Do I need a user ID to evaluate a feature flag? Only for percentage rollouts. Without a stable identifier a server SDK serves the control variation, and the hosted endpoint spreads traffic with no stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-do-two-users-get-different-variations-in-the-same-rollout/ # Why do two users get different variations? [Home](/) / [Questions](/questions/) # Why do two users get different variations? Last updated: August 3, 2026 In a percentage rollout that is the expected behavior. Each user is hashed together with the flag key to produce a bucket from 0 to 100, so a 30% rollout puts roughly three users in ten on the new variation. The same user always lands in the same bucket for the same flag. Hashing is per flag, so being inside the 30% for one tells you nothing about any other. Two colleagues compare screens, see something different, and file a bug. The rollout is almost always working exactly as configured. ## What the split is doing The user identifier and the flag key are hashed together, producing a number from 0 to 100. A 30% rollout serves the new variation to everyone below 30. Because the hash is deterministic, a user stays in the same bucket forever, across refreshes, sessions and devices. Which is the behavior you want, since nobody should be watching a feature appear and vanish between page loads. ## Why your test accounts disagree They hash differently. There is no pattern to it, and no way to reason about which side a given account will land on without checking. Give yourself a deterministic answer instead: add a targeting rule above the rollout that serves your own account the variation you want to see. Rules run first and win, so the rollout never gets a say. ```plaintext 1. email endsWith "@yourcompany.com" -> serve true 2. Fallthrough: 30% rollout ``` serve true2. Fallthrough: 30% rollout"> Now internal accounts always see the new path and the percentage still governs everyone else. ## Why the same user differs across two flags Different flags produce different hashes for the same person. If they did not, every rollout at the same percentage would hit exactly the same users, and a customer unlucky enough to sit at bucket 2 would be the guinea pig for everything you ship. Independent hashing spreads that risk. It also means you cannot line up two rollouts by setting both to 30%. ## When it really is wrong There are still two cases where the complaint is real. **No identifier.** Without a stable key there is nothing to hash. A server SDK evaluating locally falls back to the control variation, and the hosted endpoint spreads traffic with no stickiness at all, which does produce a user seeing different values on consecutive requests. [That case has its own answer](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) . **A changing identifier.** If you pass a session ID rather than a user ID, the bucket moves whenever the session rotates. Anything you hash on has to outlive the rollout. ## Widening the rollout Going from 30% to 50% widens the window. Everyone already inside stays inside, and only new users are added, so nobody loses a feature they had yesterday. If you want the hash itself rather than its consequences, [Rollout strategies](/docs/concepts/rollout-strategies/) sets out the mechanics. ## Related questions [### Do I need a user ID to evaluate a feature flag? Only for percentage rollouts. Without a stable identifier a server SDK serves the control variation, and the hosted endpoint spreads traffic with no stickiness.](/questions/do-i-need-a-user-id-to-evaluate-a-feature-flag/) [### Why did my targeting rule not match? Rules run top to bottom and the first match wins, so an earlier rule can shadow the one you are testing. The other usual cause is a missing context attribute.](/questions/why-did-my-targeting-rule-not-match/) [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-do-users-on-the-same-team-get-different-variations/ # Why do teammates get different variations? [Home](/) / [Questions](/questions/) # Why do teammates get different variations? Last updated: August 6, 2026 A rollout buckets by user key by default, and each user hashes independently. Two colleagues in the same workspace count as two separate users, so a 50% rollout splits them roughly half the time. Point the rollout at an account or organization attribute instead, and everyone sharing that value moves together. The rollout is bucketing people, and your users experience your product as a team. By default a percentage rollout hashes the individual user key. Two colleagues in one workspace are two independent users, so a 50% rollout puts them on opposite sides of it about half the time. Nothing is misconfigured, and that is exactly the trouble: your configuration describes individuals while the feature gets used by teams. It arrives as a support ticket that reads like a bug. One person describes a screen their colleague cannot find, same plan, same workspace, same afternoon. ## Bucket by the account instead A rollout can bucket by any attribute in the evaluation context, so it does not have to be the user key. Send the account identifier and point the rollout at it. Getting that attribute into the context in the first place is covered in [Targeting and segments](/docs/concepts/targeting/) . ```js client.boolVariation('new-workspace-nav', { userId: 'u_8134', account_id: 'acct_204', // the rollout buckets on this plan: 'pro', }, false); ``` Everyone sharing `acct_204` now hashes to the same bucket and moves in and out of the rollout together. Your percentage now means 20% of accounts rather than 20% of people, which for most B2B features is what you meant. Keep sending the user key as well. You still want it for user-level targeting rules, and for anything else that should stay per-person. ## What to watch Account-level rollouts are lumpier than user-level ones. Accounts vary enormously in size, so 20% of accounts might be 5% or 45% of seats depending on which ones fall in. Watch both numbers during a rollout, and expect the user-weighted figure to move in steps as large accounts cross the boundary. Changing `bucketBy` on a live rollout reassigns everybody at once, because it replaces one of the two hash inputs. Set it before you start, or accept a full reshuffle at the moment you change it. [Rollout strategies](/docs/concepts/rollout-strategies/) covers the bucketing options in full. ## When per-user is right Keep the default for anything a person experiences alone, like interface preferences or individual onboarding. Account grouping earns its place when colleagues collaborate inside the feature, or talk to each other about it. ## Related questions [### Does changing a targeting rule re-bucket users? Editing rules, weights or conditions leaves every bucket where it was. Only two changes reassign users, and both are easy to make by accident.](/questions/does-changing-a-targeting-rule-re-bucket-users/) [### Why doesn't my rollout hit the percentage I set? A 20% rollout rarely serves exactly 20% of traffic. Bucketing is deterministic per user, so small populations skew and anonymous traffic behaves differently.](/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/) [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-does-my-feature-flag-work-locally-but-not-in-docker/ # Why does my flag work locally but not in Docker? [Home](/) / [Questions](/questions/) # Why does my flag work locally but not in Docker? Last updated: August 6, 2026 The SDK key almost certainly did not reach the container. A key read from a local env file that is never passed through in your compose file or Dockerfile arrives empty, and the SDK serves your default value rather than failing loudly. After that, suspect outbound network. A container on an internal-only network cannot reach the evaluation endpoint, so the first payload never arrives. Your code did not change between the two runs. The difference is in what reaches the process, and it comes down to a missing key or a blocked route almost every time. Either one ends with an empty flag snapshot, and an empty snapshot means every call returns the default value you passed. That is the fail-safe behavior you want in production. It also makes local and containerized runs diverge without ever raising an error. ## Check the key first Usually your shell has the key and the container does not. ```bash # On the host: the key is in your shell or a local env file echo $FEATUREFLIP_SDK_KEY # sdk_server_... # In the container: nothing was passed through docker compose exec api sh -c 'echo "[$FEATUREFLIP_SDK_KEY]"' # [] ``` A local env file that your dev server loads automatically stays invisible to a container unless your compose file passes it through. Log the key’s prefix at startup, never the key itself, and you will catch this in seconds without opening a single configuration file. While you are there, confirm it is the right kind of key. A server key sees every flag in the project, a client key sees only the flags explicitly marked client-side visible. Container work often involves moving evaluation from a browser to a backend or the reverse, and the key needs to move with it. ## Then check outbound access A container attached only to an internal network cannot reach the evaluation endpoint. The SDK will keep retrying and keep serving defaults in the meantime. Confirm from inside the container, not from the host, since the container’s namespace is the one that matters. If your environment requires an egress proxy, the SDK’s HTTP client needs to know about it, and a long-lived streaming connection needs a proxy that does not buffer responses. ## The third possibility If both check out, compare which environment each run is pointed at. A development key and a production key are different keys, and a flag enabled in one is often disabled in the other. There is a separate answer on [why one key cannot serve every environment](/questions/can-i-use-one-sdk-key-for-every-environment/) if that is where you have landed. Whichever of the three it turns out to be, the reason you got no error is the same. [Reliability](/docs/concepts/reliability/) covers why an empty snapshot is treated as a survivable state instead of a fault. ## Related questions [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [### Can I use one SDK key for every environment? No. The SDK key is what selects the environment, so sharing one collapses dev and production onto the same flag configuration.](/questions/can-i-use-one-sdk-key-for-every-environment/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-does-my-flag-flash-the-wrong-value-on-page-load/ # Why does my flag flash the wrong value on load? [Home](/) / [Questions](/questions/) # Why does my flag flash the wrong value on load? Last updated: August 6, 2026 Your UI is rendering inside the window before the browser SDK has its first flag payload, and during that window every call returns the default value you passed. The reason string reads FlagNotFound rather than an error, since the flag is simply absent from an empty snapshot. Wait for initialization before rendering the gated region, or render a neutral state until the client is ready. The flash is a race, and the SDK is behaving correctly on both sides of it. A browser client starts with an empty flag snapshot. Ask it for a flag before the first payload lands and there is nothing to evaluate, so it hands back the default value you passed at the call site and reports `FlagNotFound`. Moments later the payload arrives, the same call returns the real variation, and your UI swaps in front of the user. ## Telling it apart from a config problem Both a flash and a genuinely invisible flag return your default, so check whether the value settles. ```js // Before init: nothing to evaluate, default is returned client.boolVariation('new-nav', ctx, false); // -> false, FlagNotFound await client.waitForInitialization(); // After init: the real answer client.boolVariation('new-nav', ctx, false); // -> true ``` false, FlagNotFoundawait client.waitForInitialization();// After init: the real answerclient.boolVariation('new-nav', ctx, false); // -> true"> If it settles on the right value a moment later, you have a timing problem and the rest of this applies. If it never settles, the flag is probably not marked client-side visible, which [has its own answer](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) . ## Rendering around the gap Hold the gated region back until the client is ready. Await [initialization](/docs/sdks/browser/) before that component’s first paint, or keep a loading state up until it resolves. In React the provider exposes readiness, so gate on that and leave the flag value out of it. Server-side rendering removes the gap entirely for the first paint. Evaluate on the server where you already have the user, pass the result down as part of the initial payload, and let the client SDK take over for later updates. The user then sees the correct variation immediately and never observes a swap. ## What not to do Do not paper over it with a timeout before rendering. It trades a visible flash for a visible delay on every load, including the loads that would have been fine, and it still fails on a slow connection. Picking a default that matches the common case narrows the flash without closing it. It also makes the remaining flash harder to diagnose, since it now only reaches the minority of users whose real value differs. ## Related questions [### Why can't my browser SDK see a feature flag? A flag has to be marked client-side visible before a client SDK key can receive it. Server keys see every flag, client keys see only the opted-in ones.](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### Why did my flag change take minutes to reach my app? Flag changes stream to SDKs in seconds. Up to thirty seconds late means the SDK fell back to polling. A change that never lands points to a buffering proxy.](/questions/why-did-my-flag-change-take-minutes-to-reach-my-app/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-does-my-flag-work-in-development-but-not-production/ # Why does my flag work in dev but not production? [Home](/) / [Questions](/questions/) # Why does my flag work in dev but not production? Last updated: August 3, 2026 Every environment holds its own configuration, so enabling a flag in development changes nothing in production. Targeting rules and rollout percentages are per environment as well, which means a flag can read as on in both and still serve different values. A new environment starts every existing flag in the off state deliberately, so nothing goes live by accident. Per-environment configuration is what lets you merge code behind a flag, exercise it locally, and leave production untouched until you decide otherwise. The system is doing its job here. It feels like a bug because the flag presents as a single object in the dashboard, when its state is really one value per environment. ## What is actually per environment These four are independent of each other: - Whether the flag is on - Its targeting rules - Its rollout percentage - The variation served on fallthrough A flag enabled in both development and production can still behave differently in each, because rules and percentages did not travel with the toggle. Seeing “on” in two environments tells you very little about whether they agree. ## The check Open the flag and switch environments in the dashboard. Compare all four, rather than the toggle alone. Most surprises are a rollout left at 100% in development and 5% in production, which is the correct end state of a careful rollout and looks like a broken flag from a test account. ## New environments start off When you add an environment, every existing flag is created there in the off state. That is deliberate. The alternative is a new QA environment inheriting production’s configuration and quietly enabling things nobody meant to run. The consequence is that a new environment needs its flags configured as well as its SDK key. ## When it is the key rather than the config If the flag is configured correctly in production and still does nothing, check which environment your production process actually authenticated as. The SDK key selects the environment, so a stale key in a deploy config means production is reading somebody else’s flags. [That failure has its own answer](/questions/can-i-use-one-sdk-key-for-every-environment/) . ## Promoting a flag Treat it like promoting code. Enable and exercise in development, then staging, then production, widening the rollout at each step. Nothing copies configuration between environments for you. That costs you a minute per flag and rules out a whole class of accident for good. [Environments](/docs/concepts/environments/) explains why the isolation is drawn where it is. The [environments guide](/docs/guides/environments/) walks the promotion workflow step by step. ## Related questions [### Can I use one SDK key for every environment? No. The SDK key is what selects the environment, so sharing one collapses dev and production onto the same flag configuration.](/questions/can-i-use-one-sdk-key-for-every-environment/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### Why did my targeting rule not match? Rules run top to bottom and the first match wins, so an earlier rule can shadow the one you are testing. The other usual cause is a missing context attribute.](/questions/why-did-my-targeting-rule-not-match/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-doesnt-my-rollout-hit-the-percentage-i-set/ # Why doesn't my rollout hit the percentage I set? [Home](/) / [Questions](/questions/) # Why doesn't my rollout hit the percentage I set? Last updated: August 6, 2026 Bucketing assigns each user a fixed number from 0 to 99 and counts how many land under your threshold, rather than metering traffic. With a few hundred users the observed share can sit several points either side of the configured one, and that is expected. Persistent skew usually means anonymous contexts, which the hosted endpoint spreads randomly on every call instead of bucketing. You set a flag to 20% and your dashboard shows 26%. A percentage rollout counts users, never requests, so it behaves quite differently from a traffic meter. Each evaluation hashes a per-flag salt together with the user’s key, takes the result modulo 100, and gets a number from 0 to 99. Variations are laid out in order with their weights accumulated, and the first band the number falls into wins. So a 20% rollout means “every user whose bucket is 0 through 19”, not “one request in five”. ## Why the observed share drifts The assignment is per user and permanent, so your observed percentage is a sample of however many distinct users happened to show up. At a few hundred users, landing several points off the configured number is ordinary sampling noise. At tens of thousands it tightens considerably. Activity is uneven too, so one heavy user in the enabled bucket can drag a request-weighted figure around while the user-weighted figure sits exactly where it should. Count distinct users when you check. ## When it is not noise Some skew never settles down no matter how much traffic arrives, and anonymous contexts are the first thing to check. When no user key is present, the two evaluation paths deliberately diverge: ```js // Keyed: stable bucket, same variation every time client.boolVariation('new-checkout', { userId: 'u_8134' }, false); // Keyless against the hosted endpoint: fresh random bucket per call client.boolVariation('new-checkout', {}, false); ``` Against the hosted evaluation endpoint a keyless context gets a fresh random bucket on every call, which spreads anonymous traffic across variations by weight but gives nobody a stable experience. A server-side SDK evaluating locally does the opposite and serves the first variation every time. In both cases your percentages have stopped describing people. The other candidate is a rule sitting above the rollout. Rules run in priority order and the first match wins, so users captured by a fixed-serve rule never reach the fallthrough at all. If half your traffic matches an internal-staff rule, the rollout only ever divides the remaining half. ## The fix Pass a stable identifier on every evaluation. If your users are genuinely anonymous, generate a key once, persist it in a cookie or local storage, and send it as the `user_id`. A random spread becomes real buckets off the back of that one change. Then check what sits above the rollout in your rule list before trusting the number. [Rollout strategies](/docs/concepts/rollout-strategies/) has the hash and the weight bands in detail. For the rule-ordering half of the problem, start with [Targeting and segments](/docs/concepts/targeting/) . ## Related questions [### Do users lose a feature if I lower the rollout? Yes, and which users lose it is predictable rather than random. Lowering a rollout removes the top of the bucket range, so the earliest users keep the feature.](/questions/do-users-lose-a-feature-if-i-lower-the-rollout/) [### Why do teammates get different variations? A percentage rollout buckets by individual user, so colleagues in one workspace land on different sides of it. Bucket by account to keep them together.](/questions/why-do-users-on-the-same-team-get-different-variations/) [### Why do two users get different variations? That is what a percentage rollout does. Bucketing hashes the user ID with the flag key, so the split is per user and per flag, and never lines up across flags.](/questions/why-do-two-users-get-different-variations-in-the-same-rollout/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-doesnt-my-segment-match-any-users/ # Why doesn't my segment match any users? [Home](/) / [Questions](/questions/) # Why doesn't my segment match any users? Last updated: August 6, 2026 The rule probably cannot resolve the segment at all, which fails closed and matches nobody. A client SDK never given segment data behaves the same way. Once that is ruled out, look at condition logic. Segments carry one flat condition list with a single AND or OR, so a segment built to catch two different user groups with AND matches nobody by construction. Both serve the fallthrough, so from outside you cannot tell a segment whose conditions genuinely exclude everyone from a segment the evaluator never found at all. Worth separating the two before you start editing conditions, because only one of them is a condition problem. When a rule references a segment, the evaluator resolves that segment first. If it cannot, the rule fails closed and reports no match. It deliberately does not fall back to the rule’s own condition list. An empty condition list matches everyone, and quietly switching a feature on for your entire user base is the one outcome nobody wants out of a targeting bug. ## The resolution case Browsers are where this usually turns up. ```js // Server SDK: segments arrive with the flag payload, rule resolves normally client.boolVariation('beta-dashboard', { userId: 'u_1', plan: 'pro' }, false); // Client SDK with no segment data: the rule cannot resolve, so no match client.boolVariation('beta-dashboard', { userId: 'u_1', plan: 'pro' }, false); // -> false ``` false"> If a flag works from your backend and not from the browser while both send the same attributes, check whether the segment is reaching the client at all before touching any conditions. ## The condition case If the segment does resolve, look at how it combines conditions. A segment carries one flat list plus a single operator for the whole list, unlike a targeting rule, which supports several condition groups combined with AND. Write a segment meant to capture “enterprise customers or anyone on the beta program” using AND and you have asked for people who are both, which is usually nobody at all. Switch the operator to OR, or split it into two segments. Attribute types are the other frequent culprit. A condition comparing `plan` to the string `"pro"` will not match a context sending a number, and a condition on `signup_date` needs the format the operator expects. ## Checking it quickly Evaluate one user you are certain belongs in the segment and read the reason back. A `RuleMatch` naming a different rule tells you something above it captured the user first, since rules run in priority order and the first match wins. A fallthrough reason tells you the segment rule genuinely did not match, which narrows it to the two causes above. [How to use segments](/docs/guides/segments/) covers how the condition list is put together. If the reason string points you at rule ordering instead, [Targeting and segments](/docs/concepts/targeting/) has the precedence rules. ## Related questions [### Why did my targeting rule not match? Rules run top to bottom and the first match wins, so an earlier rule can shadow the one you are testing. The other usual cause is a missing context attribute.](/questions/why-did-my-targeting-rule-not-match/) [### Why doesn't my semver targeting rule match? Semver operators parse the version string before comparing. A build number, a date, or anything else that is not a version matches nothing at all.](/questions/why-doesnt-my-semver-targeting-rule-match/) [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-doesnt-my-semver-targeting-rule-match/ # Why doesn't my semver targeting rule match? [Home](/) / [Questions](/questions/) # Why doesn't my semver targeting rule match? Last updated: August 6, 2026 The value you are sending does not parse as a semantic version. An unparseable string matches nothing at all, with no fallback to text comparison, which is why the rule looks dead instead of behaving oddly. A leading v is fine and 2.0 compares equal to 2.0.0, but a build number like 1042 or a date will never satisfy a semver condition. A semver condition parses both sides into version components before it compares anything. When the value from your evaluation context does not parse, the condition matches nothing at all. It never degrades into a plain string comparison. That is why the rule sits there inert rather than behaving strangely, and it accounts for most reports of a semver rule doing nothing. ## What parses The parser is fairly forgiving with real version strings. ```js // All of these parse { app_version: '2.4.1' } { app_version: 'v2.4.1' } // leading v is stripped { app_version: '2.4' } // missing parts compare as 0, so this equals 2.4.0 { app_version: '2.4.1-beta' } // prereleases rank below the release { app_version: '2.4.1+build7' } // build metadata is ignored for precedence // These do not, and match nothing { app_version: 1042 } { app_version: '2026.08.06' } // parses, but as a version, not a date { app_version: 'v2.4.1-rc.1 (build 90)' } ``` Numeric parts get compared piece by piece, never as one long number, so `2.10.0` correctly ranks above `2.9.0`. Version numbers of any size are safe. A prerelease ranks below its release, which catches people out constantly: `2.4.1-beta` does not satisfy “at least 2.4.1” even though it reads later. Build metadata is ignored entirely for precedence too, so `2.4.1+a` and `2.4.1+b` compare equal. ## The usual causes Mobile clients often send a build number where you wanted a version name, and the two live in different fields. A native app sending `1042` where you meant `2.4.1` produces exactly this symptom. A version sent as a number when it should be a string is the other frequent one. So is a version string carrying anything beyond the version itself, since a suffix in parentheses fails the parse for the whole value. ## Checking it Read back the raw attribute for one user whose version should match, and confirm it is a string that would parse on its own. If it does parse and the rule still misses, check what sits above it, because rules run in priority order and the first match wins. Server-side and local SDK evaluation share the same parser, so a version that matches in one matches in the other. No need to test both. You will find the comparison operators listed in [Targeting and segments](/docs/concepts/targeting/) . For building the rule itself, [How to set up targeting rules](/docs/guides/targeting-rules/) has worked examples. ## Related questions [### Why did my targeting rule not match? Rules run top to bottom and the first match wins, so an earlier rule can shadow the one you are testing. The other usual cause is a missing context attribute.](/questions/why-did-my-targeting-rule-not-match/) [### Why doesn't my segment match any users? A rule pointing at a segment fails closed when the segment cannot resolve, so it matches nobody rather than everybody. How to tell that from a bad condition.](/questions/why-doesnt-my-segment-match-any-users/) [### Why is my feature flag returning the wrong type? SDKs do not coerce types. Asking for a boolean from a string flag returns the default you passed rather than converting, so code and dashboard disagree.](/questions/why-is-my-feature-flag-returning-the-wrong-type/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-is-my-feature-flag-returning-the-default-value/ # Why is my feature flag returning the default value? [Home](/) / [Questions](/questions/) # Why is my feature flag returning the default value? Last updated: August 3, 2026 The SDK almost certainly cannot find the flag at all, so evaluation never got far enough to fail. Check the SDK key first, since it may belong to a different environment, then whether the flag is simply off there. After that comes a misspelled key, a client still initializing, a type mismatch against the variation, or a flag that is not client-side visible when you ask from a browser. Every variation call takes a default as a required argument. The SDK falls back to it under four conditions: a missing flag, an unexpected value type, a client that has not finished loading, and an evaluation error. That design buys you an outage which cannot take your app down. The cost is that a configuration mistake and a healthy fallback produce identical output. Work through these in order. The first two account for most reports. ## 1. The SDK key belongs to a different environment Each environment has its own SDK key, and the key decides which configuration your process receives. A production key will never see a flag you only created in development. Nothing errors, because from the SDK’s point of view the flag genuinely does not exist. Print the key prefix your process actually loaded: ```js console.log(process.env.FEATUREFLIP_SDK_KEY?.slice(0, 18)); // sdk_server_a1b2c3 -> server key // sdk_client_a1b2c3 -> client key ``` server key// sdk_client_a1b2c3 -> client key"> Then confirm in the dashboard that the key belongs to the environment you are looking at. A stale value in a deploy config or a `.env` file that shadows the real one is the single most common cause. ## 2. The flag is off in that environment Flags are configured per environment. On in development and off in production is the normal state during a rollout, and a disabled flag serves its off variation everywhere it is disabled. ## 3. The key is misspelled Flag keys are exact strings. `new-checkout` and `new_checkout` are two different flags, and only one of them exists. The SDK reports this as `flag-not-found` and carries on. ## 4. The client has not finished initializing Server SDKs load the full configuration once at startup. Read a flag before that completes and you get the default, correctly, because there is nothing else to serve yet. ```js await client.initialize(); // every variation call after this point reads from memory ``` In a browser or React app the equivalent is the readiness flag the SDK exposes. Gate on it before you trust a value. ## 5. The type does not match Asking for a boolean from a string flag returns the default. No coercion happens. Check that the variation type in the dashboard matches the method you are calling. ## 6. The flag is not client-side visible This one only affects browser, React, Flutter, Android and Swift SDKs. A flag has to be explicitly marked client-side visible before a client key can see it, and the SDK reports the same `flag-not-found` it would for a key that does not exist. There is a [separate answer covering exactly that case](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) . ## One thing this is usually not An unmet prerequisite. When a prerequisite is not satisfied the flag serves its **off variation**, which is a value you configured, rather than the default you passed at the call site. So if the default specifically is what you are seeing, look elsewhere. The reason string separates them cleanly, with `prerequisite-failed` on one side and `flag-not-found` on the other. For what the SDK does under every failure mode, see [Reliability and resilience](/docs/concepts/reliability/) . Cause 1 and cause 2 both come back to [Environments](/docs/concepts/environments/) , which is the better place to start if the key looks suspect. ## Related questions [### Why does my flag work in dev but not production? Flags are configured per environment, so enabling one in development changes nothing elsewhere. Rules and rollout percentages are per environment too.](/questions/why-does-my-flag-work-in-development-but-not-production/) [### Why can't my browser SDK see a feature flag? A flag has to be marked client-side visible before a client SDK key can receive it. Server keys see every flag, client keys see only the opted-in ones.](/questions/why-cant-my-browser-sdk-see-a-feature-flag/) [### What if I archive a flag still used in my code? An archived flag leaves the SDK configuration, so any remaining call site falls back to the default you passed. Remove the code first, then archive.](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-is-my-feature-flag-returning-the-wrong-type/ # Why is my feature flag returning the wrong type? [Home](/) / [Questions](/questions/) # Why is my feature flag returning the wrong type? Last updated: August 3, 2026 The SDKs do not coerce between types. If the variation holds a string and you call the boolean method, you get the default you passed and no conversion happens. The dashboard then shows the flag serving one thing while your code reports another, which reads like a stale cache and is really a type mismatch. Flags carry a type. A boolean flag serves `true` or `false`, string and number flags serve what you would expect, and a JSON flag hands back a structured object. The variation method you call has to agree with the flag you are calling it on. When it does not, the SDK returns your default. It does not attempt a conversion, and it does not raise. ## What this looks like ```js // Flag `checkout-copy` is a string flag serving "variant-b" client.boolVariation('checkout-copy', false); // -> false, not true // Correct method for the type client.stringVariation('checkout-copy', 'control'); // -> "variant-b" ``` false, not true// Correct method for the typeclient.stringVariation('checkout-copy', 'control'); // -> "variant-b""> The dashboard is correct. So is the code. Looked at separately neither shows a problem, and only putting them together breaks anything. ## Why no coercion Because every guess would be wrong somewhere. Is the string `"false"` truthy? Is an empty JSON object enabled? A rule that satisfies one codebase silently corrupts another, and a flag controlling a kill switch is the wrong place to find out. Returning the explicit default keeps the behavior predictable: you get the value you wrote at the call site, which is the same thing you would get during an outage. ## The check Open the flag and compare its type against the method name. Then check the variation values themselves. A boolean flag whose variations were entered as the strings `"true"` and `"false"` is a boolean flag in name only, and `boolVariation` will return the default for it. This is worth looking at specifically when a flag was created through the API or Terraform, where a JSON payload makes it easy to write `"true"` where `true` was meant. ## JSON flags JSON is where this shows up most, because a JSON flag is often reached for as a config bag: ```js const config = client.jsonVariation('llm-config', { model: 'gpt-4o-mini', temperature: 0.2, }); ``` The default here does double duty, acting as both the outage fallback and the shape contract for everything downstream, so populate it with real values. Code reading `config.model` out of an empty object tends to fail a long way from the flag that caused it. ## Types are fixed after creation A flag’s type is set when it is created and does not change afterwards. If you need a different one, create a new flag, migrate the call sites, and retire the old key on the [normal archive path](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) . [What are feature flags](/docs/concepts/feature-flags/) lists the four variation types and what each one serves. Why the default comes back at all belongs to [Reliability and resilience](/docs/concepts/reliability/) . ## Related questions [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### How do I test code that uses feature flags? Every Featureflip SDK ships a test client you construct with fixed flag values. No network, no initialization, no mocking the SDK surface yourself.](/questions/how-do-i-test-code-that-uses-feature-flags/) [### What if I archive a flag still used in my code? An archived flag leaves the SDK configuration, so any remaining call site falls back to the default you passed. Remove the code first, then archive.](/questions/what-happens-when-i-archive-a-flag-still-in-my-code/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/questions/why-is-my-flag-evaluation-returning-an-error/ # Why is my flag evaluation returning an error? [Home](/) / [Questions](/questions/) # Why is my flag evaluation returning an error? Last updated: August 6, 2026 The evaluator picked a variation key the flag no longer defines, which normally means a rule or fallthrough still references a deleted variation. The SDK serves the default value you passed and reports Error, declining to guess which survivor you meant. A variation that exists but holds a null value falls outside this, so the reason string reliably separates a config problem from a data one. The word covers less ground than you would think, and it has nothing to do with the network or a crashed SDK. Evaluation ran all the way to completion and selected a variation key. The flag does not define it. The usual cause is a deleted variation that something still references. A flag with three variations gets trimmed to two, a rule further down the list was still serving the third, and that rule now resolves to a key that no longer exists. The SDK will not guess which of the survivors you meant, so it serves the default value you passed at the call site and labels the reason honestly. ## Reading it ```js const detail = client.variationDetail('checkout-experiment', ctx, false); // { value: false, reason: 'Error' } ``` `Error` is a separate reason from `FlagNotFound`, which means the flag is absent from the SDK’s snapshot entirely, and from `Fallthrough`, which means everything worked and no rule matched. Server-side responses use the kebab-case spelling `error` for the same condition. A variation that genuinely exists and holds a null value is perfectly valid, so the check does a key lookup. Test the value and you cannot separate an intentional null from a missing variation. Treating that null as a fault would be its own bug. ## Finding the stale reference Open the flag’s targeting configuration and read every serve, including the fallthrough and any rules further down than the one you were looking at. You are looking for a variation key that is no longer in the variations list. The audit log is faster if the flag has been edited recently, since the variation removal and the rule that outlived it will be adjacent entries. ## Preventing it Removing a variation that a prerequisite depends on is already blocked, which rules out one whole class of this. Rules within the same flag are what need the manual check. Point every rule at a surviving variation before you delete the old one, in that order. Keep passing a sensible default at each call site too, since your users get that default on the day this happens. Every reason string the evaluator can emit is listed in the [analytics integration guide](/docs/guides/analytics-integration/) . For tracing when the variation actually disappeared, the [audit log guide](/docs/guides/audit-log/) is faster than reading the config. ## Related questions [### Why is my feature flag returning the default value? Six reasons a Featureflip flag falls back to the default you passed, ordered by how often each one is the real cause, with the check for each.](/questions/why-is-my-feature-flag-returning-the-default-value/) [### Why is my feature flag returning the wrong type? SDKs do not coerce types. Asking for a boolean from a string flag returns the default you passed rather than converting, so code and dashboard disagree.](/questions/why-is-my-feature-flag-returning-the-wrong-type/) [### Why did my targeting rule not match? Rules run top to bottom and the first match wins, so an earlier rule can shadow the one you are testing. The other usual cause is a missing context attribute.](/questions/why-did-my-targeting-rule-not-match/) [Browse all feature flag questions →](/questions/) ## Still stuck? The docs cover every SDK, and the Free plan is enough to reproduce most of these locally. [Read the docs](/docs/) [Get started free](https://app.featureflip.io/signup) --- ## Source: https://featureflip.io/changelog/ # Changelog [Home](/) / Changelog # Changelog What shipped, when it shipped, and where to read the docs for it. Newest first. Each release links to the packages and pages it added, so you can check the claim rather than take our word for it. - October 1, 2026 Integration ## Flag owners in the MCP server An agent connected to @featureflip/mcp 0.1.9 can now assign flag owners. Ask it to hand a flag to a teammate or find the stale flags nobody owns, and it calls the same owner endpoints as the Management API under the same plan rules. That means Pro and above. - A new set_flag_owner tool takes a member's email, "me" for the token's own user, or null to clear the owner - create_flag accepts ownerEmail - list_flags and find_stale_flags filter by owner with an email, "me", or "none" for unowned flags, and find_stale_flags reports each candidate's owner - update_flag changes only the fields you pass, so leaving out the description no longer erases it - Setting an owner needs Pro or above. Clearing one works on every plan [MCP server docs →](/docs/integrations/mcp/) [Flag owners guide →](/docs/guides/flag-owners/) - September 30, 2026 Platform ## Flag owners Every flag can now name the person responsible for it. The owner shows on the flag and in the flags list, you can filter by it to see what is yours or what nobody has claimed, and cleanup notices about a flag go to its owner instead of to every admin. When someone leaves, the admin removing them picks who inherits their flags in the same step. Flag owners are on Pro and above. On Free and Team, Featureflip still records each flag's creator as its owner, so a team that upgrades finds its flags already owned. - Pick an owner on the Create Flag page, or use Set owner and Change owner in the flag header - The flags list has an Owner column and a filter for Any owner, Mine, Unowned or any member - Removing a member offers Reassign their flags to, and leaving it unset makes their flags unowned - The Management API sets the owner with PUT /flags/{flag}/owner, clears it with DELETE, accepts ownerEmail when a flag is created, and filters the flag list with ?owner= - Every flag response carries an owner object, or null when the flag is unowned - Available on Pro, Business and Enterprise. Below Pro the creator is stored as owner and appears once you upgrade [Flag owners guide →](/docs/guides/flag-owners/) [Launch post: new plans, SSO and the flag lifecycle →](/blog/new-plans-sso-flag-lifecycle/) [Plans and limits →](/docs/concepts/plans-and-limits/) [Pricing →](/pricing/) - September 30, 2026 Platform ## Cleanup notices for flags that are ready to remove Featureflip now emails someone when a flag is ready to come out of your code. Once a day it checks for flags that have passed their expiry date or gone Dead, and sends a digest listing each one with the reason it qualified and a link to the flag. On Pro and above the email goes to the flag's owner. On Team, and for any flag without an owner, it goes to the organization's Owners and Admins. Cleanup notices come with Team and every plan above it. On Free, the same flags show as Expired or Dead in the flags list and through the MCP server and the API. - A flag qualifies when it passes its expiry date or goes Dead, meaning a staleness signal has held for 90 days - One digest per person per organization, sent daily at 14:00 UTC, grouped by project - Each flag is announced once, and a flag that qualifies again later waits at least 30 days for another notice - Archived flags never get notices - Turn them off under Email Preferences with the Flag cleanup notices switch, or with the unsubscribe link in any notice. Account and security email is unaffected [Cleanup notices guide →](/docs/guides/cleanup-notices/) [Launch post: new plans, SSO and the flag lifecycle →](/blog/new-plans-sso-flag-lifecycle/) [Flag expiration guide →](/docs/guides/flag-expiration/) [Stale flag detection →](/docs/concepts/stale-flags/) - September 30, 2026 Platform ## Single sign-on with your own identity provider Your team can now sign in to Featureflip through the identity provider your company already runs. Featureflip connects over OpenID Connect, so Okta, Microsoft Entra ID and any other provider that publishes an OIDC discovery document will work, and your IdP decides who gets in. New people can join automatically with a role you choose, and an Owner can require single sign-on for the whole organization. Single sign-on is on the Business and Enterprise plans. SAML and SCIM are part of Enterprise. - Connect any OIDC provider with an issuer URL, client ID and client secret, then test the round trip before you activate it - Step-by-step guides for Okta and Microsoft Entra ID, each with a Terraform configuration for the IdP side - Prove you own an email domain with a DNS TXT record, and only people on verified domains are admitted - Add new people automatically as a Viewer, Member or Admin, or admit only the people you have invited - Require single sign-on so members can only open the organization through your IdP, while Owners keep an audited recovery path - While SSO is required, only personal access tokens created during an SSO session work against the organization - People who joined through SSO can add a Featureflip password with Forgot password?, which emails them a link to set one - Available on Business and Enterprise. SAML and SCIM are on Enterprise [Single sign-on guide →](/docs/guides/single-sign-on/) [Launch post: new plans, SSO and the flag lifecycle →](/blog/new-plans-sso-flag-lifecycle/) [Okta setup guide →](/docs/guides/sso-okta/) [Microsoft Entra ID setup guide →](/docs/guides/sso-entra-id/) [Pricing →](/pricing/) - September 30, 2026 Platform ## New plans: Free for up to 3 people, and a Team tier Featureflip now has four self-serve plans. Solo is renamed Free and covers up to 3 team members, so a small team can run flags in production without paying anything. Team is new, at $49 a month or $39 billed annually, for teams of up to 5. Pro moves to $99 a month and Business to $249, and neither loses any room. Scheduled changes are now part of the paid plans. Subscriptions that started before this change keep the price they signed up at. - Free covers up to 3 team members, 1 project, 2 environments and 10 flags, and it does not expire - Team is $49 a month ($39 billed annually) for 5 team members, 3 projects, 3 environments, unlimited flags, scheduled changes, 30-day audit logs and email support - Pro is $99 a month ($79 billed annually) for 10 team members, 10 projects and 5 environments, and adds flag owners, so every flag can name the person responsible for removing it - Business is $249 a month ($199 billed annually) for 25 team members, 25 projects, unlimited environments and 90-day audit logs - Business adds single sign-on through your own identity provider over OIDC, and an organization can require it for every member - Targeting, rollouts, all 13 SDKs, the Management API, the MCP server, the Terraform provider, the audit log and automated flag cleanup stay on every plan, Free included - An existing subscription keeps its current price until you switch between monthly and annual billing, which moves it to the current rate [Pricing →](/pricing/) [Scheduled changes docs →](/docs/guides/scheduled-changes/) [Team management docs →](/docs/guides/team-management/) [Single sign-on docs →](/docs/guides/single-sign-on/) - September 24, 2026 Platform ## Outbound webhooks for configuration changes Featureflip can now call your own HTTPS endpoint whenever a flag or its configuration changes, so a deploy pipeline, chat channel or cache can react to a toggle instead of polling for it. Each subscription picks the event types it wants and can be narrowed to specific projects and environments. Every request is signed, retried when your endpoint fails, and kept in a delivery log you can resend from. Subscriptions are managed from Organization Settings, through the Management API or with the Terraform provider, and an organization gets one on Free, three on Team and Pro, and as many as it needs on Business and Enterprise. - Subscribe to any of 29 event types across flags, targeting rules, variations, segments, environments, projects, SDK keys, API tokens and the organization, or leave the list empty to receive all of them - Scope a subscription to chosen projects and environments, so a staging pipeline never hears about production toggles - Every delivery carries webhook-id, webhook-timestamp and webhook-signature headers, and the signature is an HMAC-SHA256 over the id, the timestamp and the raw body - A failed delivery is retried five times, backing off from 10 seconds to 2 hours, and any 2xx response counts as success - The delivery log shows status, attempts and the last response for each event, and any delivery can be sent again once your endpoint is fixed - A test event checks a new subscription end to end without waiting for a real change - Rotating a signing secret keeps the old one valid until you retire it, and each request is signed with every active secret during the overlap - The Management API covers the same ground at /api/v1/orgs/{org}/webhooks, including deliveries, redelivery, tests and secret rotation - @featureflip/mcp 0.1.8 adds four webhook tools, so an AI agent can list subscriptions, read the delivery log, send a test or redeliver, and create, update or rotate a subscription with an Admin token - Terraform provider 0.3.0 adds featureflip_webhook and featureflip_webhook_secret, so a subscription and its secret rotation can be reviewed in a pull request, plus a data source listing the event types [Webhooks guide →](/docs/guides/webhooks/) [Management API reference →](/docs/management-api/reference/) [MCP server docs →](/docs/integrations/mcp/) [Terraform provider docs →](/docs/integrations/terraform/#webhooks) - September 24, 2026 Platform ## Expiration dates for temporary flags A feature flag can now carry the date it is meant to come out. Set it when you create the flag or later from its header, and once that day passes Featureflip reports the flag as stale with the reason PastExpiry, right beside the traffic-based signals stale flag detection already tracks. The date is a reminder for your team, and the flag keeps serving exactly what it served before. - An optional Expires on field on the Create Flag page, plus a Set expiry control on every existing flag - Past its date, a flag is marked Stale with the reason PastExpiry, and the flag header and flags list show it as Expired - PastExpiry applies as soon as the date passes, with no 30- or 90-day observation window like the traffic-based signals - Extending the date or removing it clears the stale marking straight away - Dates are whole UTC days, and a flag expires at the end of its chosen day in UTC - A passed date alone never makes a flag a removal candidate for the flag cleanup Action, which still waits for traffic to agree - The Management API sets and clears the date at /flags/{flag}/expiry and accepts expiresAtUtc when a flag is created - @featureflip/mcp 0.1.8 adds set_flag_expiry and an expiresAtUtc option on create_flag, and find_stale_flags now reports each expired flag however recently it was edited [Flag expiration guide →](/docs/guides/flag-expiration/) [Stale flag detection →](/docs/concepts/stale-flags/) [MCP server docs →](/docs/integrations/mcp/) - September 18, 2026 Integration ## Archive a flag when its removal deploys A removal pull request merging is not the same event as its code going live, and Featureflip will not archive a flag its own traffic is still evaluating. That refusal used to fail the run. Version 1.2.0 makes it a deferred outcome that exits 0, then adds two modes that come back for the flag once the code is live. Put mode: archive-sweep on a schedule and it retries every flag whose removal merged while the flag itself is still being evaluated. The other mode, archive-on-deploy, waits for deployment_status instead, then archives the flags whose removal the deployed commit contains, asked of GitHub's compare endpoint rather than assumed. Pair the sweep with either one, because a deferral nothing retries leaves the flag live indefinitely. - A flag the platform can still see traffic for reports deferred and exits 0, instead of turning an ordinary removal merge into a red build - mode: archive-sweep runs on a schedule and retries those, finding every flag whose removal pull request merged while the flag itself is still live - Nothing is written down to mark a deferral, so the sweep has no state to reset and re-running it costs nothing - mode: archive-on-deploy fires on deployment_status and archives only the flags whose removal the deployed commit contains, asked of GitHub's compare endpoint - A deployment to any environment other than the one named in deployment-environment archives nothing and says so, as does any status that is not success - A merged removal the deploy does not contain is reported pending-deploy and left for the deploy that ships it - Both modes need GITHUB_TOKEN: the sweep asks for pull-requests: read, and archive-on-deploy asks for contents: read as well - Neither closes the case of clients you cannot update, where old versions keep evaluating a flag whatever you deploy [Flag cleanup Action docs →](/docs/integrations/flag-cleanup-action/) [v1.2.0 on GitHub →](https://github.com/canopy-labs/featureflip-flag-cleanup-action/releases/tag/v1.2.0) [Archiving once the removal is deployed →](https://github.com/canopy-labs/featureflip-flag-cleanup-action#archive-the-flag-once-the-removal-is-deployed) - September 18, 2026 Platform ## Sign in with GitHub GitHub joins Google as a way to sign in, on every plan. Click Continue with GitHub on the sign-in or sign-up screen, approve the consent screen, and Featureflip signs you in or creates your account from your verified GitHub email. GitHub uses OAuth 2.0 rather than OpenID Connect, so its sign-in response carries no verification status at all, and Featureflip reads your primary address and its verified flag from GitHub directly before it will create anything. - Continue with GitHub appears on both the sign-in and the sign-up screen, on every plan including Free - Featureflip asks for the user:email scope and reads your primary address from it, which is the only place GitHub reports whether an address is verified - A public profile address that differs from your primary one is ignored, so the account is created under the address that was actually checked - A GitHub account whose primary address is unverified is turned away rather than signed in - An existing Featureflip account under the same verified address is linked to GitHub, and either your password or GitHub works from then on - Two-factor authentication still applies, so an enrolled account is asked for its one-time code once GitHub hands you back - Enterprise SAML and SCIM are unchanged and stay on the Enterprise plan [Account security guide →](/docs/guides/account-security/) [Security at Featureflip →](/product/security/) - September 17, 2026 Integration ## OpenFeature provider for PHP PHP joins Node.js, .NET, Python, Go and Java behind the OpenFeature API. The package is featureflip/featureflip-openfeature on Packagist, and it wraps the Featureflip PHP SDK, so your call sites talk to the standard OpenFeature client and swapping vendors becomes a provider registration rather than a rewrite. Unlike the other five providers, it takes no SDK key: the PHP SDK requires a caller-supplied PSR-18 HTTP client, PSR-17 factories and a PSR-16 cache, so you build a FeatureflipClient yourself and hand it to the provider instead. - Boolean, string, integer, float and object resolution through the standard OpenFeature PHP client - targetingKey is written under both user_id and userId, so a targeting rule against either spelling matches, unless you already supplied one yourself - An unmet prerequisite surfaces as the custom reason PREREQUISITE_FAILED, because no standard OpenFeature reason models one - A DateTime-valued context attribute is converted to ISO-8601 before evaluation, recursing into nested arrays - The open-feature/sdk PHP package has no provider-events API, so flag changes arrive on the underlying SDK's poll interval rather than as a PROVIDER_CONFIGURATION_CHANGED push. Needs featureflip/featureflip-php 3.3.0 or newer - The Featureflip flag cleanup Action recognizes OpenFeature PHP reads, so a removed flag is cleaned up at those call sites too - Apache 2.0, the same license as every Featureflip SDK [OpenFeature provider docs →](/docs/integrations/openfeature/) [featureflip/featureflip-openfeature on Packagist →](https://packagist.org/packages/featureflip/featureflip-openfeature) - September 16, 2026 Integration ## OpenFeature .NET catches up on four SDK releases The .NET provider now requires Featureflip.Client 2.9.0. NuGet resolves a package reference to the lowest version that satisfies it, so anyone on 0.3.x kept loading the 2.7.0 SDK while four releases of evaluation fixes went only to people using the .NET SDK directly. Version 0.4.0 closes that gap. Before you upgrade, check your date rules. A Before or After operand has to be written as ISO-8601 or as a Unix timestamp in seconds, so a rule holding something like 05/15/2023 will stop matching. Rewrite it as 2023-05-15. - Resolves Featureflip.Client 2.9.0 instead of 2.7.0, so four releases of evaluation and delivery fixes reach OpenFeature call sites - A Before or After date operand has to be ISO-8601 or a Unix timestamp in seconds. Formats like 05/15/2023 and Jan 1 2024 resolved in .NET alone and matched nothing in the other six SDKs, so one saved rule targeted differently depending on which language read it - Date operands are case-sensitive, so 2024-01-31T09:30:00Z matches where the lowercase spelling matches nobody - An operator the SDK does not recognize now fails closed. It used to return false, which a negated condition flipped to true, so a config naming an unknown operator could serve the flag to everyone - Config fetches survive an unknown flag type, so a new type added server-side cannot blank out every flag you serve - Analytics events survive a transient failure of the events endpoint instead of being dropped, with the queue bounded at 10,000 events - The NuGet listing renders release notes, which it had never done for any version [OpenFeature provider docs →](/docs/integrations/openfeature/) [Featureflip.OpenFeature on NuGet →](https://www.nuget.org/packages/Featureflip.OpenFeature) [v0.4.0 on GitHub →](https://github.com/canopy-labs/featureflip-dotnet-openfeature/releases/tag/v0.4.0) - September 14, 2026 Integration ## Re-run a flag cleanup pull request from a comment A pull request from the flag cleanup Action can sit open while its base branch moves on, or someone pushes a fix to it by hand and wants it to try again. Version 1.1.0 takes that request as a comment. Comment @featureflip-cleanup rerun and the Action rebuilds the removal from the pull request's own base branch, then force-pushes the result. It refuses if anyone else has committed to that branch, and commenting @featureflip-cleanup recreate instead is how you tell it to go ahead and throw those commits away. Neither command takes a flag key. The same release adds a flags input that limits a sweep to the keys you name. - Comment @featureflip-cleanup rerun to regenerate a removal from the pull request's own base branch. It refuses when the branch carries commits the Action did not make - @featureflip-cleanup recreate regenerates anyway and discards those commits - Both commands push under a lease on the branch's current commit, so the push fails if someone writes to the branch mid-run - They run as mode: pr-command in their own issue_comment workflow, and only for someone with write access to the repository - A new flags input limits a sweep to the flag keys you list, and a key that is also in ignore stays skipped - In TypeScript and JavaScript modules, top-level bindings a removal leaves unused are now cleaned up, including import x = require() and a single-name destructured require [Re-running a removal from a comment →](https://github.com/canopy-labs/featureflip-flag-cleanup-action#re-run-a-removal-from-a-comment) [v1.1.0 on GitHub →](https://github.com/canopy-labs/featureflip-flag-cleanup-action/releases/tag/v1.1.0) [Flag cleanup Action docs →](/docs/integrations/flag-cleanup-action/) - September 9, 2026 Integration ## GitHub Action that removes dead flags from your source Detecting a dead flag was always the easy half. The flag cleanup Action does the other one: it reads removal candidates from the public API, deletes the flag branch in your repository, and opens one pull request per flag for you to review. Merge it and the flag can archive itself, so the code and the dashboard stop disagreeing. It runs inside your CI holding your token, and no model writes the diff. The rewrites are AST transforms, so the same input always produces the same patch. - Thirteen languages: TypeScript, TSX, JavaScript, PHP, Ruby, ERB, Dart, Java, Go, Python, Kotlin, C# and Swift - One reviewable pull request per flag, with max-prs to bound a first run and dry-run to change nothing at all - Two safety gates parse every rewritten file and discard the whole transform if it introduced breakage, so a rewrite it cannot verify never reaches a pull request - Opt into mode: archive-on-merge and the flag is archived when its removal pull request merges - Every released tag names one exact image digest, so a run stays reproducible even though @v1 moves - Source and issues at canopy-labs/featureflip-flag-cleanup-action [Flag cleanup Action docs →](/docs/integrations/flag-cleanup-action/) [Guide: automated feature flag removal →](/blog/automated-feature-flag-removal/) [Why was my flag not removed? →](/questions/why-didnt-the-cleanup-action-remove-my-flag/) [Action source on GitHub →](https://github.com/canopy-labs/featureflip-flag-cleanup-action) - September 6, 2026 SDKs ## Flag change subscriptions in the Go and Java SDKs Go and Java can now tell your code when flag configuration changes. Register a listener and it gets the keys of every flag an update touched, in one call, so you can drop a cache or log the change as it lands instead of polling for it. That puts Go and Java level with JavaScript, Node.js, .NET and Python, which picked up the same subscription in featureflip 2.7.0 on August 28. It is also what the Go and Java OpenFeature providers use to emit PROVIDER_CONFIGURATION_CHANGED. - Go 2.8.0 adds Client.OnUpdate, which takes a func(flagKeys []string) and returns a func that unsubscribes - Java 2.9.0 adds FeatureflipClient.onUpdate, which takes a FlagUpdateListener and returns a Runnable that unsubscribes - Python has had client.on_update and client.off_update since featureflip 2.7.0, released on August 28 - An update also names flags whose value moved without their own configuration changing: those whose rules reference an edited segment, and those that depend on a changed flag through a prerequisite - The initial flag load does not fire, and neither does a poll or reconnect that changed nothing - A listener that panics or throws is logged and skipped, and flag delivery carries on - Closing a client handle drops the listeners it registered [Go SDK reference →](/docs/sdks/go/#flag-change-notifications) [Java SDK reference →](/docs/sdks/java/#flag-change-notifications) [Python SDK reference →](/docs/sdks/python/#flag-change-notifications) - September 6, 2026 Integration ## OpenFeature provider for Java Java joins Node.js, .NET, Python and Go behind the OpenFeature API. The artifact is io.featureflip:featureflip-openfeature on Maven Central, and it wraps the Featureflip Java SDK, so your call sites talk to the standard OpenFeature client and swapping vendors becomes a provider registration rather than a rewrite. Register it with setProviderAndWait rather than setProvider, for the same reason as every other runtime: the plain form returns before flags have loaded, and the evaluations on the lines after it can hand back their defaults. - Boolean, string, integer, double and object resolution through the standard OpenFeature Java client - The targeting key maps to the user id that percentage rollouts bucket on, unless you supplied one explicitly - A wrong-typed flag is reported as TYPE_MISMATCH, distinctly from a genuine evaluation failure - An unmet prerequisite surfaces as the custom reason PREREQUISITE_FAILED, because no standard OpenFeature reason models one - PROVIDER_CONFIGURATION_CHANGED fires when flag configuration changes, naming the keys that moved - The Featureflip flag cleanup Action recognizes OpenFeature Java reads, so a removed flag is cleaned up at those call sites too - Apache 2.0, the same license as every Featureflip SDK [OpenFeature provider docs →](/docs/integrations/openfeature/) [featureflip-openfeature on Maven Central →](https://central.sonatype.com/artifact/io.featureflip/featureflip-openfeature) - September 6, 2026 Integration ## OpenFeature provider for Go Go joins Node.js, .NET and Python behind the OpenFeature API. The module is github.com/canopy-labs/featureflip-go-openfeature, and it wraps the Featureflip Go SDK, so your call sites talk to the standard OpenFeature client and swapping vendors becomes a provider registration rather than a rewrite. Register it with SetProviderAndWait rather than SetProvider, for the same reason as in Node and Python: the plain form initializes on a background goroutine and returns before flags have loaded. - Boolean, string, integer, float and object resolution through the standard OpenFeature Go client - The targeting key maps to the user id that percentage rollouts bucket on, unless you supplied one explicitly - An unmet prerequisite surfaces as the custom reason PREREQUISITE_FAILED, because no standard OpenFeature reason models one - PROVIDER_CONFIGURATION_CHANGED fires when flag configuration changes, naming the keys that moved. Needs the Go SDK 2.8.0 or newer - The Featureflip flag cleanup Action recognizes OpenFeature Go reads, so a removed flag is cleaned up at those call sites too - Apache 2.0, the same license as every Featureflip SDK [OpenFeature provider docs →](/docs/integrations/openfeature/) [featureflip-go-openfeature on pkg.go.dev →](https://pkg.go.dev/github.com/canopy-labs/featureflip-go-openfeature) - August 28, 2026 Integration ## OpenFeature provider for Python Python joins Node.js and .NET behind the OpenFeature API. The provider is on PyPI as featureflip-openfeature-provider, and it wraps the Featureflip Python SDK, so your call sites talk to the standard OpenFeature client and swapping vendors becomes a provider registration rather than a rewrite. Register it with set_provider_and_wait rather than set_provider. The second one returns before the first flag load has finished, and the evaluations on the lines after it can hand back their defaults. - Boolean, string, integer, float and object resolution through the standard OpenFeature Python client - targeting_key maps to the user_id field that percentage rollouts bucket on, unless you already supplied one - An unmet prerequisite surfaces as the custom reason PREREQUISITE_FAILED, because no standard OpenFeature reason models one - PROVIDER_CONFIGURATION_CHANGED fires when flag configuration changes, naming the keys that moved. Needs featureflip 2.7.0 or newer - Apache 2.0, the same license as every Featureflip SDK [OpenFeature provider docs →](/docs/integrations/openfeature/) [featureflip-openfeature-provider on PyPI →](https://pypi.org/project/featureflip-openfeature-provider/) - August 28, 2026 Integration ## The MCP server is in the official MCP Registry The Featureflip MCP server is listed in the official Model Context Protocol Registry as io.github.canopy-labs/featureflip. Clients and directories that read the registry can now find it and install @featureflip/mcp from npm without anyone copying a config block out of our docs. The listing names the package, its stdio transport and the environment variables it reads, and every release updates it automatically. - Listed as io.github.canopy-labs/featureflip, installed from the @featureflip/mcp package on npm - The listing describes the stdio transport and the three environment variables: FEATUREFLIP_TOKEN, which is required, plus the optional API URL and organization - Each release of @featureflip/mcp publishes a matching registry version, starting with 0.1.3 - Works on every plan, Free included, with a personal or service token [MCP server docs →](/docs/integrations/mcp/) [Registry entry →](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.canopy-labs/featureflip) [@featureflip/mcp on npm →](https://www.npmjs.com/package/@featureflip/mcp) - August 26, 2026 SDKs ## One date grammar across every SDK A Before or After targeting rule now reads its date the same way in every SDK. Until this release each one leaned on its own language date parser, so a single saved rule could serve one variation to a service written in C# and a different one to a service written in Go. Every SDK now accepts ISO-8601 or a Unix timestamp in seconds, and rejects everything else, including the US-style and slash-separated formats the .NET SDK used to allow. The same run of releases also fixed analytics delivery: a batch the events endpoint refuses now goes back on the queue instead of being discarded. - The JavaScript, Node.js, Python, Go, Java, Ruby, PHP and .NET SDKs share one date grammar: ISO-8601, or a Unix timestamp in seconds - A date that names no real calendar day, such as 2024-02-30 or 2023-02-29, matches nothing instead of rolling into the following month - The Management API rejects a non-conforming operand on write, so a rule saved from this release onward cannot carry one. Rewrite any older rule that uses another format - Analytics events go back on the queue after a failed send rather than being dropped, in every SDK including Android, Swift and Flutter [Targeting concepts →](/docs/concepts/targeting/) [Condition operators →](/docs/api/flags/) [SDK overview →](/docs/sdks/overview/) - July 29, 2026 SDKs ## Evaluation inspectors across the server SDKs Inspectors let you watch every flag evaluation from inside your own process. Register a callback and it fires on each variation call with the flag key, the context you passed, the value the caller received, the variation key, the reason, and whichever rule or prerequisite decided it. This is the piece you need to send exposure data to an analytics tool without hand-wrapping every call site. - onEvaluation inspectors in the JavaScript, Node.js, Python, Go, Java, Ruby and PHP SDKs - Flag-change notifications in the JavaScript and Node.js SDKs (client.on('update')) and the .NET SDK (FlagsChanged), batched into one call and resolved back through prerequisites - A served variation key the flag does not define now reports an error reason and hands back your default, instead of a success reason that was quietly wrong [Analytics integration guide →](/docs/guides/analytics-integration/) [SDK overview →](/docs/sdks/overview/) - July 25, 2026 Integration ## Terraform provider on the Registry The Featureflip provider is published on the Terraform Registry at v0.1.0. Projects, environments, flags, variations, targeting rules, segments and SDK keys are all resources, so your flag configuration goes through the same version control and review that the rest of your infrastructure already does. The provider is open source in its own repository and talks to the public API, which means it runs against the free plan too. - Resources for projects, environments, feature flags, per-environment flag config, segments and SDK keys - Data sources for reading an organization, project, environment, flag or segment that Terraform does not manage - terraform import for flags and projects that already exist, so adoption does not start with a teardown - Source and issues at canopy-labs/terraform-provider-featureflip [Terraform provider docs →](/docs/integrations/terraform/) [Guide: feature flags as code →](/blog/feature-flags-as-code-terraform/) [Terraform Registry →](https://registry.terraform.io/providers/canopy-labs/featureflip/latest) - July 14, 2026 Integration ## OpenFeature providers for Node.js and .NET OpenFeature is the CNCF vendor-neutral API for feature flags. Two providers now sit behind it, @featureflip/openfeature-node on npm and Featureflip.OpenFeature on NuGet. Your call sites talk to the OpenFeature client instead of ours, which turns swapping vendors into a provider registration rather than a rewrite. That cuts both ways and we ship it anyway, because a team weighing a small vendor is right to want a cheap exit. - Boolean, string, number and object resolution through the standard OpenFeature client - Context mapping from OpenFeature targetingKey to the Featureflip user key, with reason and error mapping preserved - Tracking forwards through to the OpenFeature track() API - Apache 2.0, the same license as every Featureflip SDK [OpenFeature provider docs →](/docs/integrations/openfeature/) [Guide: OpenFeature and feature flags →](/blog/openfeature-feature-flags/) - July 10, 2026 Integration ## Manage flags from Claude Code and Cursor The @featureflip/mcp server puts flag management inside any editor or agent that speaks the Model Context Protocol. You say what you want in English and the agent calls the Management API for you: create a flag, check where it is enabled, add a targeting rule, archive the dead ones. Most established vendors ship an MCP server now. Ours works on the free plan. - One npx command and a config block, authenticated with a Personal Access Token - Works in Claude Code, Cursor, and every other MCP client - Tools for creating, toggling, targeting, archiving and auditing flags, plus finding the stale ones - Included on the Free plan, not held back for paid tiers [MCP server docs →](/docs/integrations/mcp/) [Guide: feature flags from your AI editor →](/blog/feature-flags-mcp-server/) [Model Context Protocol glossary entry →](/glossary/model-context-protocol/) - July 9, 2026 API ## Management API, on every plan Everything the dashboard does is available over REST at api.featureflip.io. Projects, environments, flags, variations, toggles, targeting rules, segments and SDK keys each have endpoints under /api/v1, authenticated with a bearer token. It is on every plan, the free one included, on the view that a configuration you cannot read out programmatically is a configuration you do not really own. - Personal Access Tokens for people, organization-scoped Service Tokens for CI - Idempotency-Key on creates, so a retried request cannot double-write - X-RateLimit headers on every response and a frozen, machine-readable error envelope - Cursor pagination and a generated OpenAPI reference for every endpoint [Management API docs →](/docs/management-api/overview/) [Quickstart →](/docs/management-api/quickstart/) [API reference →](/docs/management-api/reference/) [Guide: the feature flag management API →](/blog/feature-flag-management-api/) Per-version detail for each SDK, including patch releases, lives in that package's CHANGELOG on [GitHub](https://github.com/canopy-labs) . Longer write-ups of the bigger releases are on the [blog](/blog/) . ## Try what shipped The API, the MCP server, and the Terraform provider are all on the Free plan. No card. [Get started free](https://app.featureflip.io/signup) [Read the docs](/docs/) --- ## Source: https://featureflip.io/about/ # About Featureflip — Feature Flags for Small Teams # About Featureflip Feature flag infrastructure for teams the enterprise vendors ignore. We're a small team building Featureflip — production-grade feature flags for developers who want sub-millisecond evaluation, transparent pricing, and SDKs they can read without signing an NDA. No four-figure monthly minimums, no mandatory sales calls, no annual contracts. Just the boring, fast primitive most teams actually need. ## Why Featureflip exists Feature flagging became its own software category by selling to enterprise. The leading platforms start at four-figure monthly minimums, gate basic capabilities behind annual contracts, and route every conversation through a sales team before you can see a price. That model works for the Fortune 500. It doesn't work for the rest of us — the indie developers, the two-person startups, the side projects, the consultants shipping production code for clients on tight margins. Teams that want feature flags as a developer primitive, not a procurement cycle. We built Featureflip for that gap. Self-serve signup, transparent pricing, sub-millisecond evaluation, thirteen open-source SDKs. The kind of feature flag service we wanted to use ourselves — billed monthly, no minimums, no demo gating. Featureflip isn't trying to replace the enterprise feature flag platforms for the Fortune 500. It's the option for everyone they politely ignore. ## About Canopy Labs Canopy Labs LLC is an Atlanta-based software company. Canopy Labs builds Featureflip — feature flag infrastructure designed for small teams, indie developers, and anyone who wants production-grade flag tooling without the enterprise tax. Canopy Labs also maintains thirteen open-source SDKs covering JavaScript, Node.js, Python, .NET, Java, Go, PHP, Ruby, Swift, Flutter, Android, browser, and React. All Apache-2.0 licensed, all on GitHub — read the source, file an issue, send a PR. The SDKs are how Featureflip gets into a codebase; the hosted service is what makes them useful with zero infrastructure on the customer's end. Canopy Labs is bootstrapped and independently operated. ## What we believe ### Pricing should be transparent. Our pricing page tells you what Featureflip costs. There's no "contact us" tier. There's no demo required to see numbers. You can sign up, evaluate flags, and decide whether to pay us — all without talking to a human. Pricing transparency isn't a marketing position; it's how we want to be sold to. We extend the same courtesy to everyone who shows up at our pricing page. ### Feature flags are infrastructure. Feature flags belong in the same mental category as Redis, Postgres, or a queue — boring, fast, well-documented primitives that don't try to be the whole platform. Our job is to make flag evaluation reliable, fast, and easy to integrate. Bundling every adjacent product into the same SKU is how categories get bloated and slow. Focus is a feature. ### Performance is the baseline, not a premium tier. Sub-millisecond evaluation is what every team should get. There's no "performance plan" upcharge. The SDKs run with cached configs locally, so flag evaluation is in-memory and never blocks a request. Slow flag systems were built for marketing teams, not the request path. We treat the request path as sacred because we ship code on it too. ### SDKs should be open source. All thirteen of our SDKs are Apache-2.0 licensed on GitHub. Read the code before you depend on it. Fork it if your edge case isn't supported. File an issue if something's wrong, send a PR if you've got a fix. The relationship between a feature flag service and a customer's codebase shouldn't be opaque — flag evaluation logic ships in your binary, and you should be able to audit it. ### Documentation should answer your question better than a demo would. We'd rather you read the docs and decide than sit through a sales call. The docs cover quickstarts, SDK references, evaluation rules, segments, environments, and the API — the same things a sales engineer would walk you through, but on your schedule and at your reading speed. If the docs can't answer a question your team has, that's a documentation bug; tell us. People ask us to back up the speed claim more than any of the others, so we publish the measurement behind it. Over the 30 days to September 19, 2026, 99.5% of evaluations on our production traffic finished inside a millisecond, with a mean of 0.054 ms. The [SDK page](/product/features/sdks/) shows what that timing covers, and the parts it deliberately leaves out. ## Get in touch Email us, use the contact form, or just sign up and try it. No credit card, no sales call, no demo required. For technical questions, the [docs](/docs/) cover most cases — and if they don't, that's a doc bug we want to hear about. [Get Started Free](https://app.featureflip.io/signup) [contact@featureflip.io](mailto:contact@featureflip.io) [Contact form](/contact/) --- ## Source: https://featureflip.io/blog/ # Blog # Blog Notes on feature flags, releases, and engineering from the Featureflip team. - [## Feature Flag Owners, Expiry Dates, SSO and New Plans Free now covers 3 people and Team is new at $49. Flags get expiry dates and named owners, someone hears when a flag is done, and Business adds SSO. September 30, 2026 · - release - cleanup - ownership - pricing - sso](/blog/new-plans-sso-flag-lifecycle/) - [## Feature Flag Cleanup Tools Compared (2026) Feature flag cleanup tools split three ways: mark the flag, let a model rewrite the code, or apply deterministic transforms. Which one fits your codebase. September 17, 2026 · - cleanup - debt - automation](/blog/feature-flag-cleanup-tools-compared/) - [## Automated Feature Flag Removal: What a Bot Must Refuse Detecting a dead feature flag is easy. Deleting it safely is not. What automated feature flag removal has to refuse in order to be worth trusting. September 9, 2026 · - cleanup - debt - automation](/blog/automated-feature-flag-removal/) - [## Trunk-Based Development with Feature Flags: What CI Misses Trunk-based development with feature flags moves risk from merge day to flag-flip day. A green trunk only proves the off path works. Here is the rest. August 28, 2026 · - trunk-based-development - ci-cd - best-practices](/blog/feature-flags-trunk-based-development/) - [## Self-Hosted Feature Flags: When Running Your Own Wins Self-hosted feature flags trade a license fee for an availability problem: your kill switch ends up inside the blast radius it exists to protect you from. August 4, 2026 · - self-hosting - architecture - best-practices](/blog/self-hosted-feature-flags/) - [## GitOps for Feature Flags: How the Workflow Actually Runs Run feature flag config through git: where it lives, what a reviewer checks, how CI applies it, and why some configuration drift is correct and should stay. August 1, 2026 · - gitops - automation - best-practices](/blog/gitops-feature-flags/) - [## OpenFeature Feature Flags: Node, .NET, Python, Go, Java, PHP OpenFeature feature flags in Node.js, .NET, Python, Go, Java and PHP: set up the Featureflip provider, map evaluation context, keep call sites vendor-neutral. July 31, 2026 · - openfeature - sdks - integrations](/blog/openfeature-feature-flags/) - [## Feature Flags as Code: Managing Featureflip with Terraform Manage feature flags as code with the Featureflip Terraform provider: declare projects, flags, and targeting in .tf files, and know what to leave out. July 30, 2026 · - terraform - automation - tutorials](/blog/feature-flags-as-code-terraform/) - [## Feature Flag Outages: How SDKs Recover and Resync A feature flag outage is survivable when SDKs evaluate from cache and fall back to your defaults. The costly failure is an SDK that never reconnects after. July 12, 2026 · - reliability - sdks](/blog/feature-flag-outage-recovery/) - [## MCP Server for Managing Feature Flags in Claude Code The Featureflip MCP server lets Claude Code, Cursor, and other AI agents create flags, ramp rollouts, and clean up stale ones without leaving your editor. July 11, 2026 · - ai - mcp - automation](/blog/feature-flags-mcp-server/) - [## Feature Flags for AI Apps: Roll Out a New LLM Model Safely Feature flags for AI apps: put the model, prompt, and settings behind one flag, roll a new LLM out with sticky bucketing, and keep a fail-safe kill switch. July 10, 2026 · - ai - tutorials](/blog/feature-flags-for-ai-apps/) - [## Feature Flag Management API: Automate Flags with REST Create projects, flags, and targeting rules programmatically with the Featureflip feature flag API. Seed flags in CI, sync environments, build internal tools. July 9, 2026 · - api - automation](/blog/feature-flag-management-api/) - [## Feature Flag Naming Conventions: A Practical Guide Feature flag naming conventions that hold up: the anatomy of a good key, how to enforce it in CI, how to scale it by team size, and how to fix bad names. July 1, 2026 · - naming - governance - fundamentals](/blog/feature-flag-naming-conventions/) - [## Dark Launch and Soft Rollout: Ship Code Before the Feature A dark launch ships a feature to production with nobody seeing it. A soft rollout then reveals it a few users at a time. How to run both with feature flags. June 30, 2026 · - dark-launch - deployments - rollouts](/blog/dark-launches-and-soft-rollouts/) - [## Python Feature Flags: A Fork-Safe Gunicorn and uWSGI Setup Python feature flags without the pre-fork trap: local in-memory evaluation, why Gunicorn and uWSGI silently freeze flags, and where to build the client. June 29, 2026 · - python - tutorials](/blog/feature-flags-in-python/) - [## Go Feature Flags: In-Process Evaluation, No cgo or Sidecar Go feature flags with a pure-Go SDK: no cgo, no dependencies, in-process evaluation with no per-request network call, goroutine-safe reads, clean shutdown. June 26, 2026 · - go - golang - tutorials](/blog/feature-flags-in-go/) - [## Node.js Feature Flags: Local Evaluation, Long-Lived Server Node.js feature flags in a long-lived server: one shared client, local in-memory evaluation, readiness gating on boot, and a clean shutdown on SIGTERM. June 25, 2026 · - nodejs - tutorials](/blog/feature-flags-in-nodejs/) - [## Next.js Feature Flags: Server-Side Evaluation, No Flash Next.js feature flags without the hydration flash: evaluate in a Server Component or getServerSideProps, then seed the client hook so it keeps updating live. June 24, 2026 · - nextjs - tutorials](/blog/feature-flags-in-nextjs/) - [## React Feature Flags: Provider, Hooks, and Live Updates React feature flags with a provider and a useFeatureFlag hook that re-renders on change, plus user targeting and tests that never touch the network. June 24, 2026 · - react - tutorials](/blog/feature-flags-in-react/) - [## Feature Flag Flat Pricing vs Per-Seat and Usage-Based Feature flag flat pricing versus per-seat and usage-based: how each model scales as your team and traffic grow, with a real cross-vendor cost comparison. June 23, 2026 · - pricing - comparison](/blog/feature-flag-pricing-explained/) - [## Build vs Buy Feature Flags: A Decision Framework Build vs buy feature flags: a homegrown one starts as an if-statement, then grows into targeting, SDKs, audit logs, and cleanup nobody owns. What each costs. June 21, 2026 · - build-vs-buy - fundamentals](/blog/build-vs-buy-feature-flags/) - [## Progressive Delivery Explained: The Two-Pillar Playbook Progressive delivery has two pillars: release progression and progressive delegation. How feature flags turn a deploy into a dial you can turn up slowly. June 3, 2026 · - progressive-delivery - deployments - rollouts](/blog/progressive-delivery-explained/) - [## Feature Flag Best Practices: 8 Rules for Production Teams Eight feature flag best practices for production teams: lifecycle tagging, flat decisions, both-state testing, progressive rollouts, observability, and cleanup. May 27, 2026 · - best-practices - lifecycle - fundamentals](/blog/feature-flag-best-practices/) - [## Prerequisite Flags: Gate One Flag on Another, No Glue Code Featureflip supports first-class prerequisite flags. A child flag stays off until its parent serves the right variation, with no application-side gating. May 27, 2026 · - prerequisites - dependencies - release](/blog/prerequisite-flags/) - [## Feature Flag Anti-Patterns: 9 Mistakes Behind Real Outages Nine feature flag anti-patterns behind real outages — Knight Capital's $460M loss, Slack's day-long cascade, Google Cloud's June 2025 crash. Each with a fix. May 6, 2026 · - anti-patterns - fundamentals - incidents](/blog/feature-flag-anti-patterns/) - [## Feature Flag Cleanup: A Playbook for Paying Down Flag Debt Most feature flags are never removed, and one reused bit cost Knight Capital $460M. The four-step playbook to clean up flag debt safely. April 27, 2026 · - cleanup - debt - fundamentals](/blog/feature-flag-cleanup/) - [## Feature Flags vs Environment Variables: A Practical Guide Env vars configure where the process runs; flags decide what each request sees. Decision rule, comparison table, and 4 production gotchas inside. April 25, 2026 · - config - fundamentals](/blog/feature-flags-vs-environment-variables/) --- ## Source: https://featureflip.io/blog/automated-feature-flag-removal/ # Automated Feature Flag Removal: What a Bot Must Refuse [Blog](/blog/) # Automated Feature Flag Removal: What a Bot Must Refuse Detecting a dead feature flag is easy. Deleting it safely is not. What automated feature flag removal has to refuse in order to be worth trusting. September 9, 2026 · Updated September 24, 2026 · - cleanup - debt - automation Every feature flag platform can tell you which flags are dead. Detection is a database query against evaluation traffic, and it has been a solved problem for years. Deleting the flag from your source code is the harder half, and the handful of tools that attempt it disagree about how it should work. That gap is wider than it looks. A stale-flag report is a list. Lists do not get worked. The flag stays in the code because removing it means opening the file, reading the branch, deciding which arm survives, deleting the other one, and convincing yourself you did not just change behavior in production. That is twenty minutes of careful work for a change that ships nothing, and it loses to every other item in the sprint, every sprint, forever. The useful question is whether a tool can be trusted to remove a flag without a human reading every diff. Most of the answer lives in what such a tool declines to do. **Key Takeaways** - Detection and removal are different problems. Detection reads telemetry, removal rewrites your source, and only one of them can break production. - Automated removal is no longer rare. What varies is whether the same input always produces the same diff, and whether the tool can name the shapes it will refuse before it runs. - A flag removal that produces valid, compiling code can still be wrong. Mock stubs are the clearest case, and the compiler has nothing to say about them. - Correctness has to be enforced outside the rewrite rules, by something that parses the whole file afterward and throws the work away if it looks wrong. - The parser you choose decides whether the tool works at all. A syntax check with a high false-positive rate produces no diffs forever and reads as “nothing to clean up.” - The list of shapes a cleanup tool refuses to touch is the specification. A short refusal list usually means the hard cases have not been found yet. ## 1. The half that got solved, and the half that did not Flag detection works because the platform already has the data. Every evaluation is a request, requests get counted, and a flag whose count has been zero for ninety days is a strong candidate for deletion. Featureflip exposes this through `find_stale_flags`, and so does most of the market in some form. As of September 2026, Featureflip also reads a second signal besides traffic: an [expiration date](/docs/guides/flag-expiration/) on the flag itself. Once that day passes, the flag is reported stale within the hour, without the 30- or 90-day hold that each traffic signal needs before it counts. That alone opens no pull request. The cleanup Action waits for traffic to agree, because a date only records what someone planned, and a typo in one should never make a live flag look safe to delete. Then the report lands, and nothing happens. This is why flag debt is so durable. Removing a flag is not one edit. You delete the read, then fold the branch it guarded, then remove the variable it was assigned to, then delete the import if nothing else uses it, then check whether the value was exported to some other module, then run the tests, then convince a reviewer none of it changed behavior. Each step is easy. The sequence is tedious, it carries real risk, and the payoff is a smaller file. Automating the report was the easy part, and the market stopped there for years. That is changing, and the question has moved with it. Automated removal is no longer rare. What differs now is how the removal happens: a language model reading your code and proposing an edit, or a transform that parses the syntax tree and applies rules to it. Both open a pull request. The second kind produces the same diff every time, and can tell you in advance which shapes it will refuse to touch. That second kind is what we built. The [Featureflip flag cleanup Action](/docs/integrations/flag-cleanup-action/) runs in your CI, reads dead flags from the public API, and opens one pull request per flag across thirteen languages. Merging that pull request can archive the flag it removed, so the code and the flag list stop drifting apart. The rest of this post is about what a tool like that has to refuse, because the refusals are what make it safe to leave switched on. ## 2. What “safe” has to mean before you let a bot commit The obvious bar for an automated rewrite is that the result compiles. It is also nowhere near enough. Consider a test that stubs a flag read with Mockito: ```java when(client.boolVariation("checkout-v2", ctx, false)).thenReturn(true); ``` The flag is dead and serves `true`, so the naive rewrite folds the read to its value: ```java when(true).thenReturn(true); ``` That compiles. Javac has no objection, the test suite runs, and the assertion that used to depend on a stubbed flag now depends on nothing at all. The stub silently stopped stubbing. Worse, the flag key is gone from the file, so no future run of the tool will ever look at this line again. The failure is permanent and invisible. Mocking libraries do this because they stub by performing the call and capturing the invocation it registers. The text of the read is the payload. Fold it and you have deleted the payload while leaving the wrapper standing. Go makes the same mistake impossible to miss, by accident: ```go m.EXPECT().BoolVariation("checkout-v2", ctx, false).Return(true) ``` Folding that read leaves `true.Return(true)`. Go’s `bool` is a builtin with no method set, so a selector on a boolean literal is broken code by construction, and the build fails immediately. Java hides the same error and Go cannot. One language got a compiler diagnostic for free and the other needs the tool to know the names of the stubbing entry points. The honest version of this story is that the Go case shipped before it was refused, and a customer’s build was what reported it. Most of the guards in a tool like this were added the same way, after something got through. ## 3. Two gates that sit outside the rewrite rules The tempting design is to make each rewrite rule guard itself: match only the shapes you are sure about, and the output is safe by construction. This does not hold up. A tree-sitter query written as a list of safe forms will, twice, let a form outside the list through, because the list was written by someone who did not think of it. So the guarantees belong somewhere else, in code that parses the entire file after the rewrite and knows nothing about which rule produced it. **The first gate re-parses every rewritten file** and discards the transform if it introduced a syntax error, put a reserved word where a name should be, stranded a keyword where an expression has to be read, or let two surviving statements fuse together under JavaScript’s automatic semicolon insertion. The discard is all-or-nothing across every file in the change, because a partially applied rewrite is worse than none. All four checks run differentially against the input, which is the part that makes them usable. A file the grammar already disliked stays eligible, and only newly introduced breakage is rejected. Without that, any repository with one unusual file would see the whole run refuse itself. Measured across 463 real TypeScript and TSX files and 833 simulated deletions, in both semicolon-terminated and semicolon-free style, that gate produced zero false positives. **The second gate runs before the rewrite** and asks a narrower question: is the name bound to this flag read bound anywhere else in the same file? If it is, the rules that inline the constant are withheld for that file, whatever the other binding looks like. It keys off the grammar’s binding fields, which are a small closed set, rather than off initializer node types, which are an open-ended set that TypeScript keeps growing. That is what makes it sound without doing real scope analysis. Both gates are conservative on purpose. A same-named variable in an unrelated function is enough to withhold the cleanup, and the cost of that caution is one leftover `const on = true;` for someone to delete by hand. Neither gate is a correctness proof, and saying otherwise would be a lie. Three of the first gate’s four checks ask whether the result still parses as intended, and a wrong rewrite can parse perfectly. They close specific, known failure classes. A new failure class needs a new check, and until someone writes it, the diff is the only thing standing in the way. ## 4. The parser decides whether the tool works at all One implementation detail decides more about this tool than any of the rewrite rules do. The first gate needs something that can tell a broken file from a working one. For JavaScript and TypeScript the obvious candidate is `node --check`, which is free, already installed, and purpose-built. It was evaluated and rejected, because it parses JavaScript rather than TypeScript. Run against 431 real TypeScript files from a production codebase, it reported 286 of them as syntax errors. A syntax gate that rejects two thirds of a healthy codebase never produces a diff, and the tool reports that there is nothing to clean up. The four files tree-sitter flagged were handled by checking differentially against the input. Think about what that failure looks like from the outside. The gate is doing its job, every candidate file is judged broken, every transform is discarded, and the tool cheerfully reports zero changes on every run. Nobody files a bug, because the output is indistinguishable from a clean codebase. A tool that is wrong in this direction is far more dangerous to trust than one that crashes. ## 5. The refusal list is the specification Once the gates are in place, the remaining design work is deciding what the tool will not touch. This list is long, and its length is the point. **Your own wrapper is the common case, and it is left alone.** Most real codebases do not call the SDK at the read site. They call `useFlag("checkout-v2")` or `isOn("checkout-v2")`, some thin function that adds logging or a default. Matching is by method name plus key string, so a wrapper is invisible, and the tool reports that it has nothing to change for a flag your code reads in forty places. That is the correct answer, and calling it a gap gets the risk backwards. Rewriting an unknown function because its first argument happened to be a flag key is how you delete a real call. It is also the single most common reason [a cleanup run reports no changes](/questions/why-didnt-the-cleanup-action-remove-my-flag/) for a flag you can grep for everywhere. **An exported binding stays.** Given `export const on = client.boolVariation("checkout-v2", ctx, false)`, the read is replaced by its value and the declaration is kept. The modules that import that binding do not contain the flag key, so they are never in the candidate set, and the tool has never parsed them. Removing the export would break files it cannot see. **A Python read bound at module or class level refuses the whole flag.** Same reasoning, different outcome. Inside a function the binding is removed and its references replaced. At module level the name is public API, and leaving `use_legacy = True` standing would be permanent, because the key is gone and no later run will revisit it. So the tool declines the flag entirely and says which file stopped it. **A read inside a C# expression tree is untouchable**, because the lambda describes the call instead of performing it, and a mocking library reads the text at run time. The tell is not the method name, it is that the read’s receiver is the lambda’s own parameter, which covers any library taking an `Expression<>` while still rewriting a genuine `Func<>` lambda that reads off a captured client. There are more. Optional calls, non-boolean accessors, un-awaited OpenFeature reads that hand back a promise instead of a value, keys held in reassignable bindings, keys hoisted to a constant in a different file. Each row exists because folding it would change behavior, and each one was decided in advance instead of being discovered by a customer. ## 6. What running it actually looks like The [Featureflip flag cleanup Action](/docs/integrations/flag-cleanup-action/) runs inside your CI, in your repository, holding your token. It reads removal candidates from the public API and opens one pull request per flag. It supports TypeScript, TSX, JavaScript, PHP, Ruby, ERB, Dart, Java, Go, Python, Kotlin, C# and Swift. ```yaml name: Featureflip flag cleanup on: schedule: - cron: '0 9 * * 1' workflow_dispatch: {} permissions: contents: write pull-requests: write jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: canopy-labs/featureflip-flag-cleanup-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: api-token: ${{ secrets.FEATUREFLIP_API_TOKEN }} org: my-org project: my-project staleness: dead dry-run: true ``` Three properties decide whether a team leaves this switched on. **It reads from the flag platform and writes nothing.** A Viewer token scoped to one project is enough for this job. Nothing archives, toggles, or deletes a flag. Removing the code is proposed as a pull request, and what happens to the flag afterward stays a human decision. Section 7 covers the opt-in workflow that changes this, and why it deliberately runs somewhere else. If the question you are actually weighing is [which credential ends up with commit access](/questions/is-automated-flag-removal-safe-to-let-into-my-repo/) , that one is answered on its own page. **Start with dry-run: true.** It computes and prints every diff without contacting GitHub at all. Every refusal a real run can produce is reachable in a dry run and reported identically, so the preview cannot approve something the next real run turns down. The default is `false`, which means omitting it opens real pull requests on the first run. That is deliberate and it is documented, but read it twice before you copy the snippet without the flag. **One run is capped.** `max-prs` defaults to ten. A project with two hundred dead flags would otherwise get two hundred branches on the first morning, and if that run was mistaken, all two hundred were. Nothing is dropped permanently, because flags that already have a branch are not proposed again and the next run continues where the last one stopped. ## 7. The flag itself, after the code is gone Merging the pull request deletes the code. The flag stays in Featureflip, live, until somebody archives it, and that is one more thing to remember at the exact moment the work feels finished. It is a small step, which is why it gets skipped, and skipping it leaves a flag list that slowly stops matching the codebase. `mode: archive-on-merge` handles it, as a second workflow on a different trigger: ```yaml name: Featureflip archive on merge on: pull_request: types: [closed] jobs: archive: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - uses: canopy-labs/featureflip-flag-cleanup-action@v1 with: mode: archive-on-merge api-token: ${{ secrets.FEATUREFLIP_ARCHIVE_TOKEN }} org: my-org project: my-project ``` Keeping it in a separate workflow is deliberate. Archiving needs a token that can write to your flags, while reading removal candidates needs one that cannot, and separate workflows can carry separate secrets. The weekly job that scans your repository keeps a token whose worst case is disclosing flag names. It works out which flag from the branch name, which the Action encodes reversibly when it opens the pull request. Nothing is read from the title or the body. Edit them freely, and squash the merge if you like. Close a pull request without merging it and nothing is archived. Declining a removal is a decision, and it has to leave the flag exactly as it was. Archiving can also refuse, and the message says which case you are in. A flag that another live flag lists as a prerequisite cannot be archived until that prerequisite is gone. Neither can one with a scheduled change still pointing at it. Both arrive after the code has already merged, which makes them a note about leftover work. ## 8. Where this leaves flag debt The four-step cleanup loop does not change. You still detect, triage, remove, and prevent, and the [cleanup playbook](/blog/feature-flag-cleanup/) still describes the mechanics of doing it by hand, which you will need for the flags a tool refuses to touch. What changes is which step is expensive. Removal was the step that consumed a careful engineer’s afternoon and therefore never happened. When it arrives as a reviewable pull request with one flag in it, the cost drops to reading a diff, and reading a diff is something teams already do all day. Retiring the flag behind it costs nothing at all once you have switched that on. That leaves triage as the expensive step, which is a better place for human attention anyway. Whether a flag is a finished release, an abandoned experiment, or a permanent operational switch is a judgment about intent that no static analysis can make. Deleting the code once you have decided is mechanical. Several vendors now automate that deletion, in ways that differ more than their feature lists suggest. If you are choosing between them, [how the cleanup tools actually differ](/blog/feature-flag-cleanup-tools-compared/) compares what each one uses to write the diff. ## The shorter version - Detecting dead flags is a solved problem. Removing them safely is not, and the gap is why flag debt survives. - Automated removal is no longer rare. Whether a run is repeatable, and whether the tool declines the shapes it cannot handle, is what separates one from another. - A rewrite that compiles can still be wrong. Mock stubs fold into code the compiler accepts and the test suite no longer tests. - Correctness lives outside the rewrite rules: a differential re-parse of every changed file, plus a file-scoped check on shadowed bindings. - The syntax gate’s parser choice is load-bearing. A high false-positive rate produces silence, which reads as a clean codebase. - The list of shapes the tool refuses is the specification. Wrappers, exported bindings, module-level Python, and expression trees are all deliberate declines. - Run it in dry mode first, cap the pull requests, and give the scanning job a read-only token. - Archiving the flag once its removal merges is opt-in, runs as its own workflow, and carries its own write-scoped secret. ## Frequently asked questions ### Can feature flag removal be fully automated? The code removal can be, for the shapes a tool can rewrite with confidence. The decision cannot. A tool can tell that a flag has served one value for ninety days and can produce a correct diff that deletes the dead branch, but whether that flag was a finished release or a kill switch you intend to keep is a judgment about intent. That is why removal arrives as a pull request instead of a commit. ### What happens if the tool cannot safely remove a flag? It leaves the code alone and says so. Depending on the shape, that means either no diff for that flag at all, or a refusal that names the file and exits with a non-zero code. A wrapper function around the SDK produces no change, and a mock stub holding the flag read refuses the whole flag. Neither case produces a partial rewrite. ### Does automated cleanup touch my feature flags themselves? Only if you switch that on. By default the Action reads removal candidates over the public API using a token you supply, and a read-only Viewer token scoped to a single project is enough. It archives, toggles, and deletes nothing, and retiring the flag after the pull request merges stays your call. A separate opt-in workflow, `mode: archive-on-merge`, archives a flag once its removal pull request is merged. That one needs a token with write access, so it runs on its own trigger and carries its own secret. The read-only token stays where it is. ### Which languages does automated flag removal support? TypeScript, TSX, JavaScript, PHP, Ruby, ERB, Dart, Java, Go, Python, Kotlin, C# and Swift. Coverage is per flag-read shape, not per language, so a file in a supported language can still contain a read the rules decline to rewrite. ### How is this different from a stale flag report? A report tells you which flags are dead and leaves the work with you. Most flag platforms produce one. The difference is whether anything acts on it: a list of forty stale flags competes with feature work and loses, while forty small pull requests compete with nothing and get reviewed. Featureflip includes stale flag detection and automated removal on every plan, including the free one, because flag debt is not a problem that only large teams have. [Start with a free account](https://app.featureflip.io/signup) and point the Action at your repository in dry-run mode to see what it would propose. --- ## Source: https://featureflip.io/blog/build-vs-buy-feature-flags/ # Build vs Buy Feature Flags: A Decision Framework [Blog](/blog/) # Build vs Buy Feature Flags: A Decision Framework Build vs buy feature flags: a homegrown one starts as an if-statement, then grows into targeting, SDKs, audit logs, and cleanup nobody owns. What each costs. June 21, 2026 · Updated September 14, 2026 · - build-vs-buy - fundamentals Every homegrown feature flag system starts the same way: a boolean column, an `if` statement, and a deploy. It takes an afternoon, it works, and for a while it’s the cheapest thing in the codebase. The build-vs-buy question almost never turns on whether your team *can* build a flag system. Of course you can. It turns on whether a flag platform is the thing you want to be maintaining in two years, and what it actually costs once that afternoon’s `if` statement has to grow up. That growing-up is where the estimate goes wrong. The first feature behind a flag is trivial. The second team that wants targeting, the first 2 a.m. kill switch, the audit log a customer’s security review asks for, the SDK for the language the mobile team uses, and the 200 stale flags nobody remembers: those are the system. This post lays out a cost-honest framework for the decision, including the cases where building your own genuinely is the right call, and how to move off a homegrown system if you already built one. **Key Takeaways** - The day-one cost of a homegrown flag is near zero. The multi-year cost is targeting, percentage rollouts, an audit log, multi-language SDKs, a propagation layer, observability, and cleanup tooling that someone has to own. - The decision is rarely about capability. It’s about total cost of ownership and opportunity cost: is reinventing a flag platform the best use of your engineers? - There are real reasons to build: flags are your product, you need air-gapped isolation no vendor can satisfy, or you only need a static toggle that’s the same for every user (that last one is configuration, not a flag platform). - Software maintenance runs 40–80% of a system’s total lifecycle cost (Robert Glass, [*Facts and Fallacies of Software Engineering*](https://blog.codinghorror.com/revisiting-the-facts-and-fallacies-of-software-engineering/) ). A flag platform you build is mostly maintenance you’ve signed up to own indefinitely. - Already built one? Move off it one flag at a time behind a single read function, and delete the dead flags first, because a deleted flag never has to be migrated. ## ”Build” doesn’t mean the if-statement. It means the platform. A feature flag is a runtime branch. A feature flag *platform* is everything that makes that branch safe to operate at scale, and the gap between the two is where build estimates quietly fail. Martin Fowler and Pete Hodgson make the distinction precise: the *toggle point* is the one line that reads the decision; the *toggle router* is the configuration, targeting, and evaluation logic behind it ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Building the toggle point is the afternoon. Building the router is the project. Here’s the accretion. You ship the first flag as a config boolean. Then a second team wants it on for staging but off in production, so you add per-environment values. Then product wants it for one beta customer, so you add per-user targeting. Then someone wants a 5% rollout, so you add deterministic bucketing. Then a customer’s auditor asks who turned a flag on, so you add an audit log and roles. Then the feature misbehaves and you need it off in seconds, not a redeploy, so you add a streaming propagation layer. Then the mobile team needs it in Swift and the data team needs it in Python, so now you maintain SDKs. None of these is hard in isolation. Together they’re a product. The green sliver is the part everyone estimates. The blue bars are the part that turns a toggle into a platform you have to operate. The point isn’t that any one capability is unattainable. It’s that the build estimate is almost always scoped to the green bar, and the blue bars arrive as a stream of “can we also” requests over the following year. By the time you’ve answered all of them, you’ve written a flag vendor’s product as a side quest. ## The cost that doesn’t show up in the build estimate The build estimate prices the build. It rarely prices the *operation*, and operation is where a flag platform actually spends your money. Software-engineering research has held for decades that maintenance, not initial construction, dominates a system’s cost: Robert Glass put it at 40 to 80 percent of total lifecycle spend ([Facts and Fallacies of Software Engineering](https://blog.codinghorror.com/revisiting-the-facts-and-fallacies-of-software-engineering/) ). For a flag platform, that slope is maintenance, on-call, SDK upkeep across languages, security review of the evaluation path, and the engineering attention pulled away from the product your customers pay for. The figures get concrete when a vendor publishes them: Unleash, an open-source flag company, says it has put more than 22 person-years into building its platform, and reports one customer, Wayfair, was spending over $3 million a year operating a homegrown system before replacing it ([Unleash](https://www.getunleash.io/blog/feature-flags-build-buy) , 2024). Those are vendor numbers, so weight them accordingly. The order of magnitude is the point: this is not a two-sprint project. Two costs hide inside that slope. The first is debt. Flags rot whether you build or buy, and most are never removed. Pete Hodgson’s canonical treatment frames toggles as “inventory which comes with a carrying cost,” something teams have to actively work to keep low ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). A bought platform at least ships staleness detection and a removal workflow to manage that inventory. A homegrown one inherits the same carrying cost with a blank page where the tooling should be, so building the cleanup machinery also lands on your team. The second hidden cost is reliability. DORA’s research links trunk-based development and loosely-coupled architecture to elite delivery performance ([DORA](https://dora.dev/capabilities/trunk-based-development/) ), and feature flags are how teams practice trunk-based development without long-lived branches. That payoff only lands if the flag layer is dependable: the fail-open evaluation, the last-known-good caching, the sub-second propagation. That reliability engineering is the hard part, and it’s the first thing a quick internal build skips. Illustrative, not to scale. The build line's slope is maintenance, on-call, and SDK upkeep: the costs absent from the original build estimate. The license is rarely the deciding number. Read the shape, not the axis. A subscription is a line you can predict; a homegrown platform is a slope that keeps climbing because every new language, every security review, and every stale-flag pileup adds to it. The honest version of the build estimate isn’t “two sprints to ship v1.” It’s “two sprints to ship v1, then a standing claim on a fraction of an engineer forever.” If flags are not the thing your company sells, that standing claim is the real price. ## When building your own is the right call Buying isn’t automatically correct, and pretending otherwise would make this a sales pitch instead of a framework. There are three situations where building genuinely wins, and a fourth that looks like building but isn’t. **Build when feature management is your product.** If you’re a platform company whose customers buy flag delivery, or you have flag requirements so specific that no vendor models them, then a flag system isn’t undifferentiated heavy lifting. It’s the differentiator. Build it, staff it, and treat it like the product it is. **Build when isolation rules out any vendor.** Air-gapped environments, classified networks, and certain regulated workloads can’t send evaluation context to a third party, even a self-hosted one with the right certifications. If a hard compliance boundary forbids the data leaving your perimeter and a vendor’s self-hosted option doesn’t satisfy your auditors, the build path may be the only path. Just price the *whole* platform from the chart above, not the afternoon. **Build when you have a platform team to own it.** The maintenance slope only works if someone’s job is to absorb it. A company with a dedicated developer-experience or platform-engineering team that already owns internal tooling can amortize a flag system across that mandate. A six-person product team without a platform function cannot, and shouldn’t pretend the slope will be free. **The fourth case isn’t really “build” at all.** If you only need a static on/off that is identical for every user and never needs targeting, percentage rollout, or runtime change, you don’t need a flag platform. You need a [config value in an environment variable](/blog/feature-flags-vs-environment-variables/) . That’s not building a flag system; it’s correctly recognizing you don’t have a flag problem. The mistake is dressing a `ENABLE_X=true` env var up as a “feature flag system” and then discovering, the first time you want a 10% rollout, that you built the wrong thing. ## When buying is the right call (the common case) For most teams, flags are infrastructure, not product, and the right move is to buy. The argument is opportunity cost: the same engineers who would build and babysit a flag platform could be shipping the features your customers actually pay for. Engineering leaders already report losing 5 to 15 hours per developer each week to undifferentiated work ([Cortex](https://www.cortex.io/report/the-2024-state-of-developer-productivity) , 2024), and a homegrown flag platform is a standing addition to that pile rather than a one-time line item. Buying hands you the full capability set on day one. The targeting, the rollout discipline, the audit log, the observability, and the cleanup tooling all arrive built, instead of being assembled incident by incident over a year. Buying also front-loads the practices that homegrown systems learn the hard way. A bought platform comes with progressive rollout, both-state safety, and a kill switch already designed, which means you adopt [feature flag best practices](/blog/feature-flag-best-practices/) by default rather than rediscovering each one after it bites you. The cautionary tale here is Knight Capital, whose in-house toggle reused a deprecated flag bit and triggered a $460 million loss in 45 minutes ([Henrico Dolfing](https://www.henricodolfing.ch/en/case-study-4-the-440-million-software-error-at-knight-capital/) , 2019). That failure mode, and the other eight in our [anti-patterns post](/blog/feature-flag-anti-patterns/) , is exactly the kind of thing a hardened platform is built to prevent and a quick internal build tends to reproduce. The standard objections to buying are real, so address them directly rather than waving them off: | Objection to buying | The honest answer | | --- | --- | | ”It’s too expensive” | Per-seat vendors do get expensive. Flat-priced tools don’t, and the comparison is against the fully loaded build cost, not zero. | | ”We’d have to self-host” | Some vendors force self-hosting; many don’t, and moving off a self-hosted flag tool is a common reason teams switch. Cloud-first tools remove the ops burden entirely, and the hosted-versus-self-hosted trade-off has its own breakdown. | | ”Remote evaluation is slow” | Local-evaluation SDKs evaluate in-process from a cached, streamed config, so per-request cost is sub-millisecond. | | ”Vendor lock-in” | The OpenFeature standard makes the SDK layer portable, so switching cost is a config change, not a rewrite. | That last row matters more than it looks. OpenFeature standardizes the evaluation API across vendors, so “what if we want to leave” stops being a rewrite and becomes a provider swap. The [Node.js, .NET, Python, Go, Java and PHP provider setup](/blog/openfeature-feature-flags/) shows what that swap looks like in code, including the parts the standard doesn’t carry across. Lock-in is a weaker argument against buying than it used to be. ## How to actually run the decision The decision comes down to four questions, asked in order, and the first “yes” gives you the answer. Is feature management your product? Do you only need a static toggle that’s the same for everyone? Do you have both a hard isolation requirement and a platform team to own the build? If none of those fire, buy. Four questions, asked in order. Most teams reach the bottom box, because for most teams flags are infrastructure rather than product. Notice what the tree doesn’t ask: “can your team build it?” That question is a trap, because the answer is almost always yes, and it’s the wrong question. Capability was never the constraint. Ownership cost was. ## Moving off a flag system you already built The tree is for teams still deciding. Many reading this made the call years ago and are now paying every cost the estimate left out. A second team asks for per-user targeting and hears it’s on the backlog. Turning something off at 2 a.m. means a redeploy, because the flag values ship inside the build. When a customer’s security review asks who changed a flag last Tuesday, the only record anyone can find is a commit to a YAML file. The mobile team wants Swift. And nobody can say which rows in the flags table still gate anything. Sound familiar? Then the slope in the chart above is already on your bill. If fear of the move is what’s keeping you there, that fear is reasonable, because swapping every flag at once over a weekend is a bet few engineering leads would sign off on. Martin Fowler calls the safer route a [strangler fig](https://martinfowler.com/bliki/StranglerFigApplication.html) . You grow the replacement around the old system one piece at a time until nothing reads from it, then turn the old one off. ### Delete the dead flags before you move anything Start with a list. Export every key the old system stores, then search the codebase for each one, and you’ll usually find a long tail of flags that have served the same value to everyone for months. Those are finished. Take the check out of the code and drop the row, and that flag is off the migration list for good. Kill switches and plan gates are built to last, so they’re the ones worth moving. The [cleanup playbook](/blog/feature-flag-cleanup/) has the triage buckets and the two-PR removal that keeps this safe, and both apply to a homegrown store unchanged. ### Put one function in front of every read If every call site already goes through one helper, something like `isEnabled('checkout-v2', user)`, the migration happens inside it. If there’s no such helper, adding one is the first real change, because it gives you a single spot to decide where each flag’s value comes from. Moved keys go to Featureflip. The old store answers for the rest until their turn comes: ```ts const MOVED = new Set(['checkout-v2', 'search-rerank']) export function isEnabled(key: string, user: User): boolean { const legacy = legacyFlags.get(key) ?? false if (!MOVED.has(key)) return legacy return client.boolVariation(key, { user_id: user.id }, legacy) } ``` The old value doubles as the SDK default. A key Featureflip can’t answer for serves exactly what it served yesterday. Create every new flag in Featureflip from the day the helper ships, so the old store stops growing while you drain it. ### Move live flags one at a time Recreate each surviving flag in Featureflip with the state it has today in every environment, and add its key to `MOVED`. Then give it a day. Copying a few hundred flags by hand is how the two sides drift apart, which is why the import belongs in a script against the [Management API](/docs/management-api/quickstart/) . Once a key has moved, its old row is only a fallback, and every real change to that flag happens in Featureflip. When the last one is across, replace the old-store lookup with a fixed default and delete the store. ### Leave half-finished rollouts for last Featureflip buckets users by hashing each ID with a salt it generates for the flag at creation, and your system hashed with something else entirely. The two assignments don’t line up. Move a flag sitting at 30% and roughly seven in ten of the users inside it land outside Featureflip’s 30%, while about as many new users land in. So wait until a partial rollout reaches 0% or 100%, where there’s nobody to reshuffle. If one can’t wait, move it anyway and expect the same share of users to keep seeing it, though mostly different people than before. ### Let the cleanup finish itself On a homegrown system, removing a finished flag waits until someone has a spare afternoon. Once the flag lives on Featureflip, its evaluation traffic marks it dead, and the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) in your CI opens a pull request that deletes the code path it gated, one flag per PR. You can keep the helper. List it in the Action’s `accessors` input and calls through it get matched like direct SDK reads, as long as the key is a string literal in the first argument. Turn on the archive-on-merge workflow as well and merging the PR archives the flag, so the dashboard stops listing a flag the code no longer reads. A key built from a variable is one of the shapes it declines, and a declined flag is reported by name. ## What “buy” looks like without the usual objections Featureflip exists to make the “buy” path the obvious one by removing the objections in the table above. Each plan is one flat monthly price with nothing metered: no charge for active users, service connections, evaluation volume, or overage. Your traffic can triple and the invoice stays put. There’s no proxy or cluster to self-host, so you adopt it without taking on ops. Server-side SDKs evaluate in-process against a cached copy of your flags, and a check on the request path never waits on a network call, which keeps per-request cost sub-millisecond. Counting browser and mobile, there are [13 official SDKs](/docs/sdks/overview/) . And the capabilities that a homegrown build accretes over a year, [per-user targeting](/docs/concepts/targeting/) , [percentage rollouts](/docs/concepts/rollout-strategies/) , [an audit log on every change](/product/features/audit-log/) , per-evaluation events, and a one-click [kill switch](/glossary/kill-switch/) , are there from the first flag you create. A framework like this is no use if it flatters the vendor who wrote it. If your situation is one of the genuine build cases above, a vendor is the wrong tool and we’d rather you know that. For a team whose product isn’t feature flags, a focused platform with nothing metered beats financing the slope on that build line. If you’re weighing specific vendors, the [compare pages](/compare/) lay out where Featureflip fits against LaunchDarkly, ConfigCat, Flagsmith, Unleash, GrowthBook, and PostHog: whether a focused flag tool [beats bolting flags onto PostHog’s analytics suite](/compare/featureflip-vs-posthog/) , how flat pricing [holds up against GrowthBook’s per-user seats](/compare/featureflip-vs-growthbook/) , and what you take on [running Unleash’s open-source edition yourself](/compare/featureflip-vs-unleash/) . If LaunchDarkly is the incumbent you’re weighing against, the [head-to-head on pricing and feature coverage](/compare/featureflip-vs-launchdarkly/) has the full breakdown. Teams that have already decided to leave get a dedicated [guide for teams leaving LaunchDarkly](/alternatives/launchdarkly-alternative/) , and the [pricing page](/pricing/) has the flat numbers in full. ## The shorter version Build-vs-buy for feature flags is almost never a question of capability, because any competent team can write an `if` statement over a config value. It’s a question of total cost of ownership. The first flag is an afternoon. The platform it has to become (targeting, percentage rollouts, an audit log, SDKs for every language, a streaming propagation layer, observability, and cleanup tooling) is a standing claim on your engineers that the build estimate never priced. Build when feature management is your product, when a hard isolation rule plus a platform team make it viable, or recognize that a static toggle identical for every user is just an environment variable. For everyone else, the engineers who’d maintain a homegrown flag platform are better spent on the product your customers buy, and a vendor with nothing metered hands you the whole capability set, plus the best practices, on day one. If you already built one, delete its dead flags first, then move the rest across one at a time behind a single helper, holding half-finished rollouts until they reach 0% or 100%. ## Frequently asked questions ### Should we build or buy a feature flag system? Buy, unless one of three things is true: feature management is your actual product, a hard isolation rule forbids any vendor and you have a platform team to own the build, or you only need a static toggle identical for every user (which is an environment variable, not a flag platform). For most teams flags are infrastructure, not product, so building reinvents undifferentiated work. ### How much does it cost to build an in-house feature flag system? Far more than the initial build estimate, because the estimate prices the first `if`-statement and skips the platform around it. Maintenance, not construction, dominates software cost over time: Robert Glass put it at 40 to 80 percent of total lifecycle spend. Add on-call, SDK upkeep across languages, and security review of the evaluation path. For scale, Unleash reports investing more than 22 person-years building its own flag platform ([Unleash](https://www.getunleash.io/blog/feature-flags-build-buy) , 2024). ### When does building your own feature flags make sense? When feature management is your product or core differentiator, when an air-gapped or regulated environment rules out any vendor and you have a dedicated platform team to absorb the maintenance, or in the narrow case where you only need a static on/off and recognize it as configuration. Outside those cases, the opportunity cost of building usually outweighs the subscription you’d avoid. ### Isn’t a feature flag just an if-statement? The toggle point is. The platform behind it isn’t. Martin Fowler and Pete Hodgson distinguish the toggle point (the one line that reads the decision) from the toggle router (targeting, configuration, evaluation) that decides ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Per-user targeting, percentage rollouts, an audit log, multi-language SDKs, and second-by-second propagation are the router, and that’s the part you’d be building. ### Does buying a feature flag tool create vendor lock-in? Less than it used to. The [OpenFeature](https://openfeature.dev/) standard defines a vendor-neutral evaluation API, so the SDK layer is portable and switching providers is a configuration change rather than a rewrite. Pick a vendor that supports the standard and “what if we want to leave” stops being a serious objection to buying. ### How do you migrate off a homegrown feature flag system? One flag at a time. Delete the flags that have finished their job before anything else, because there’s no reason to carry them over. Then route every read through a single helper that sends migrated keys to the new platform and the rest to the old store, passing the old value as the SDK default. Leave partial rollouts for last and move them at 0% or 100%, since the new platform buckets users with its own hash. Once you’ve decided to buy, the practices that keep flags from becoming next quarter’s incident are the same whether the platform is yours or a vendor’s. The [feature flag best practices pillar](/blog/feature-flag-best-practices/) covers the eight rules production teams converge on, and the [feature flags vs environment variables guide](/blog/feature-flags-vs-environment-variables/) draws the line for the static-toggle case above. One cost a homegrown system rarely budgets for is the flags nobody owns, which is why [removing dead flags without anyone scheduling the work](/blog/automated-feature-flag-removal/) is worth understanding before you commit either way. Featureflip is the platform this framework points toward: sub-millisecond local evaluation on the server, 13 SDKs, an audit log on every change, a kill switch built in, and dead flags removed by pull request, on a monthly plan with nothing metered. If you’re moving off a system you built, [start with the Free plan](https://app.featureflip.io/signup) and bring one flag across first. It’s free forever for one project. --- ## Source: https://featureflip.io/blog/dark-launches-and-soft-rollouts/ # Dark Launch and Soft Rollout: Ship Code Before the Feature [Blog](/blog/) # Dark Launch and Soft Rollout: Ship Code Before the Feature A dark launch ships a feature to production with nobody seeing it. A soft rollout then reveals it a few users at a time. How to run both with feature flags. June 30, 2026 · - dark-launch - deployments - rollouts The safest release is one where the risky part already happened days ago, quietly, while nobody was looking. That is the idea behind a dark launch: put the new code in production, run it against real traffic, and keep it invisible until you have proof it works. The reveal, when it comes, is a configuration change, not a deploy. A dark launch answers one question (“does this code survive production?”) and a soft rollout answers the next one (“is this feature right for users?”). They are different techniques for different risks, they happen in that order, and feature flags are what make both of them cheap. This guide covers what each one is, when to reach for it, and how to run the pair without leaving a mess behind. **Key Takeaways** - A **dark launch** deploys a feature to production turned off or invisible, so the code runs under real conditions before any user sees it. Facebook coined the practice in 2008 by load-testing Chat from real browsers with no UI drawn ([Meta Engineering](https://engineering.fb.com/2008/05/13/web/facebook-chat/) ). - A **soft rollout** is the next step: reveal the feature to a small slice of users, watch, then widen. Dark launch lands the code; soft rollout decides who sees it and when. - The two manage different risks. Dark launch targets infrastructure and correctness risk at 0% exposure. Soft rollout targets feature and product risk at 1% exposure and up. - Feature flags are the control plane for both: ship at off, validate, ramp by percentage, and reverse with a one-click [kill switch](/glossary/kill-switch/) instead of a redeploy. - The one rule that ties them together: a launch flag is a temporary flag. Remove it once the feature is fully live, or it becomes [flag debt](/glossary/flag-debt/) . **How to use this guide.** This is the practical how-to for the two techniques and the order you run them in. For the one-paragraph definitions, see the [dark launch glossary entry](/glossary/dark-launch/) and the [percentage rollout entry](/glossary/percentage-rollout/) . For the wider strategy these two sit inside, read the [two-pillar progressive delivery playbook](/blog/progressive-delivery-explained/) . For the failure mode they replace, see the big-bang release in [feature flag anti-patterns](/blog/feature-flag-anti-patterns/) . ## 1. What a dark launch actually is A dark launch deploys a feature into production without releasing it to users. The code is live, running or ready to run behind a flag held at 0%, but no one can see it. You validate it under real production conditions first, then make it visible when you decide. The point is to separate two events that a normal release fuses together: shipping the code, and turning it on. That separation is the whole value. When deploy and release are the same moment, every deploy is an act of faith. You find out whether the new code holds up at the worst possible time, with 100% of users already on it. A dark launch moves the discovery earlier and shrinks the audience for it to zero. If the new service buckles under load, the only witnesses are your dashboards. The canonical example is also the origin of the term. When Facebook built Chat in 2008, they could not safely guess whether their backend would survive tens of millions of concurrent users. So before any chat box appeared, “Facebook pages would make connections to the chat servers, query for presence information and simulate message sends without a single UI element drawn on the page” ([Meta Engineering](https://engineering.fb.com/2008/05/13/web/facebook-chat/) ). Real browsers generated real load against the real backend, invisibly. Dark-launch testing ran from February 2008; the first visible message went out in April, by which point the scaling questions were already answered. The release itself was almost boring, which is exactly the goal. ## 2. The two flavors of dark launch “Dark launch” covers two related techniques, and it helps to name them, because they validate different things. A **back-end dark launch** exercises a new code path without surfacing its results. You run shadow traffic against the new service, dual-write to the new data store, or compute a value and throw it away, purely to see how the system behaves under production load and against production data. This is the Facebook Chat pattern. It tests capacity, latency, and correctness before a single user depends on the answer. A **hidden-feature dark launch** deploys the finished user-facing feature behind a flag held at off, then flips it on later. Nothing about the new UI is exposed; the code simply waits in production. This is less about load and more about decoupling the deploy schedule from the release decision, so a feature can ship on a Tuesday and launch on a Thursday with no second deploy in between. Both rely on the same property: the code reaching production is one decision, and the feature becoming visible is a separate one. Feature flags give you that separation directly. The branch lives behind a named flag, the deploy carries it to production dormant, and exposure becomes a value you edit rather than a release you cut. ## 3. Dark launch vs soft rollout vs canary vs A/B test These four terms get used as if they were interchangeable. They are not, and choosing well starts with knowing which question each one answers. A **dark launch** asks: does this code work in production at all? Exposure is 0%. A **soft rollout** (often called a soft launch) asks: is this feature good for users? Exposure starts small and grows. A [canary release](/glossary/canary-release/) asks: is this new *version* of the service healthy? It routes a small slice of traffic to the new build and promotes or aborts based on error rates. An [A/B test](/glossary/a-b-testing/) asks: which variant performs better on a metric? It splits users into groups and measures the difference. The cleanest way to keep them straight is by what each one exposes at its first step. Three of these limit the blast radius at step one; the big-bang release is the pattern the other three exist to avoid. In practice you combine them. A team dark-launches a new pricing service to prove it under load, then puts the user-facing change behind a soft rollout once the backend is trusted, and may run an A/B test on the variant during the ramp. The techniques compose because they manage different risks at different layers. ## 4. How to dark launch with feature flags The mechanics are simpler than the theory. Wrap the new path in a flag, ship it at off, and choose what “on” means for your validation. For a hidden-feature dark launch, the code is just a branch behind a named decision: ```js // The flag is held at off in production. The code is live but invisible. if (client.boolVariation('checkout-v3', user, false)) { renderCheckoutV3(); } else { renderCheckoutV2(); } ``` The deploy carries `checkout-v3` to production with every user resolving to `false`. Nothing changes for anyone. When you are ready to validate against real people, you turn it on for a cohort you control: internal staff and dogfooding accounts first, via a [targeting rule](/docs/concepts/targeting/) rather than a percentage. That is still a dark launch in spirit, because the public sees nothing, but now your own team is exercising the real feature in the real environment. For a back-end dark launch, “on” means run the new path and discard the result. You call the new service, compare its output to the old one or simply measure its latency and error rate, and serve the old result to the user regardless. The flag controls whether the shadow path executes, so you can ramp the synthetic load up and down by editing config, exactly as Facebook ramped its invisible Chat traffic. Either way, the rollback story is the same and it is the reason flags fit this job. If the dark-launched code misbehaves, you set the flag back to off. There is no rebuild, no redeploy, and no waiting. With Featureflip, that change [streams to every connected SDK](/docs/concepts/rollout-strategies/) in sub-second, and evaluation happens in-process, so holding a feature dark costs you nothing on the hot path. ## 5. The soft rollout: turning the dark launch up slowly Once the code has proven it survives production, the dark launch is done and the soft rollout begins. This is where you stop asking “does it work?” and start asking “is it good?” You reveal the feature to a small, growing share of real users and watch the metrics that tell you whether to keep going. A soft rollout is a [percentage rollout](/glossary/percentage-rollout/) with discipline attached. You move the flag from 0% to 1%, then 5%, then 25%, then 100%, pausing at each step long enough to read the signal. The exposure curve looks like a staircase, and the flat segment before it (the dark launch) is the part most write-ups skip. The shaded window is the dark launch: code in production, zero users exposed. The staircase that follows is the soft rollout, each step a config change you can pause or reverse. Two details separate a soft rollout from just yanking a slider. First, the bucketing has to be sticky: a user who lands in the 5% should stay in it as you widen, not flicker in and out on every evaluation. Featureflip’s [rollout strategies](/docs/concepts/rollout-strategies/) hash on a stable user key so assignment is deterministic and consistent across every replica and SDK. Second, each step is gated on a metric, not a timer. You widen because error rates held and the business number looked right, not because an hour passed. If a step looks wrong, you do not roll forward; you flip back to the previous percentage, and because that is a config push, recovery is immediate. ## 6. When to reach for which The decision is about the risk you are actually carrying, not about which technique sounds most thorough. | You are worried about… | Reach for | Exposure at step one | | --- | --- | --- | | New service surviving production load | Back-end dark launch | 0% (shadow traffic) | | Decoupling a deploy from its launch date | Hidden-feature dark launch | 0% (flag held off) | | A new feature being wrong for users | Soft rollout | 1–5% of users | | A new build or version being unhealthy | Canary release | small traffic slice | | Which of two variants performs better | A/B test | split cohorts | Most genuinely risky launches use the first and third together: dark launch to retire the infrastructure risk, then soft rollout to retire the product risk. The mistake is skipping straight to a wide release because the staging environment looked fine. Staging is not production, and the gap between them is precisely the gap a dark launch is built to close. The pattern these techniques exist to retire is the [big-bang release](/blog/feature-flag-anti-patterns/) : ship to everyone at once and hope. ## 7. The pitfalls that bite The techniques are simple; the ways they go wrong are predictable. Four are worth naming. **Leaving the flag in.** A launch flag is a temporary flag. Once the feature has been at 100% for a release cycle or two, the flag is dead weight, and dead flags accumulate into the kind of inventory that carries a real maintenance cost ([Martin Fowler](https://martinfowler.com/articles/feature-toggles.html) ). Schedule its removal at the same time you create it. The mechanics of doing that without breaking anything live in the [feature flag cleanup playbook](/blog/feature-flag-cleanup/) . **Treating a dark launch as a substitute for testing.** Running code invisibly in production validates load, latency, and integration with real data. It does not validate that the feature is correct. Unit tests, integration tests, and review still do their jobs; a dark launch is the layer you add on top for the questions only production can answer. **Dual-write divergence.** A back-end dark launch that dual-writes to a new store can silently drift from the old one. If you are running both paths, compare their outputs and alert on the difference, rather than assuming the new path agrees just because it did not error. **Soft rollout without sticky bucketing.** If assignment is not deterministic, a user can see the new feature on one request and the old one on the next. That is worse than a clean rollout, because it looks like a bug to the user and to your support team. Make sure the rollout hashes on a stable key before you widen past internal staff. Get those four right and the pair becomes routine. The broader discipline of rolling out well, across all of it, is covered rule by rule in [feature flag best practices](/blog/feature-flag-best-practices/) , and the deeper reason this all works, separating the act of deploying from the act of releasing, is the same boundary explored in [feature flags vs environment variables](/blog/feature-flags-vs-environment-variables/) . ## The shorter version A dark launch deploys a feature to production while keeping it invisible, so the code runs under real load and against real data before any user depends on it. Facebook gave the technique its name in 2008 by load-testing Chat from real browsers with no UI drawn. A soft rollout is the next step: once the code is proven, you reveal the feature to a small, growing share of users, pausing at each percentage to read the metrics before you widen. The two manage different risks in order, infrastructure and correctness first at 0% exposure, then product fit from 1% upward, and feature flags are what make both cheap, because shipping dark, ramping by percentage, and rolling back are all configuration changes rather than deploys. Run a back-end dark launch when you are worried about load, a hidden-feature dark launch when you want to separate the deploy from the launch date, and a soft rollout when the feature itself might be wrong for users. Keep the bucketing sticky, gate each step on a metric instead of a timer, and remove the flag once the feature is fully live so today’s launch does not become next quarter’s debt. ## Frequently asked questions ### What is a dark launch? A dark launch deploys a feature into production without releasing it to users. The code is live, running or held behind a flag at 0%, but invisible. You validate it under real production conditions first, then make it visible when you choose. Facebook popularized the practice in 2008 by load-testing Chat from real browsers before any chat UI appeared ([Meta Engineering](https://engineering.fb.com/2008/05/13/web/facebook-chat/) ). ### What is the difference between a dark launch and a soft rollout? A dark launch is about deploying without releasing: getting code safely into production while it stays invisible, to test load and correctness. A soft rollout is about how you then reveal it, expanding from a small cohort of users to everyone. They compose in that order: dark launch to land the code, then soft rollout to ramp exposure. A dark launch sits at 0% exposure; a soft rollout starts around 1–5% and grows. ### Is a dark launch the same as a canary release? No. A [canary release](/glossary/canary-release/) routes a small slice of live traffic to a new build and promotes or aborts based on its health, so users in the canary do see the change. A dark launch exposes nothing to users; the code runs invisibly to test the system itself. A canary asks whether a new version is healthy; a dark launch asks whether the code survives production at all. ### How do feature flags enable dark launches and soft rollouts? Feature flags decouple deploy from release. You ship the code behind a flag held at off (the dark launch), then turn exposure into a value you edit: a [targeting rule](/docs/concepts/targeting/) for internal staff, then a [percentage rollout](/glossary/percentage-rollout/) you ramp from 1% to 100% (the soft rollout). Rollback is a one-click config push rather than a redeploy, which is what makes both techniques safe to use routinely. ### When should I use a dark launch? Use a back-end dark launch when you are unsure a new service or code path will hold up under production load or against production data, the case Facebook Chat faced. Use a hidden-feature dark launch when you want to separate a deploy from its launch date, shipping the code early and flipping it on later. In both cases the goal is to retire infrastructure and correctness risk before any user is exposed. Featureflip gives you the control plane for dark launches and soft rollouts: ship a feature behind a flag held at off, validate it against internal cohorts with targeting rules, ramp exposure with sticky percentage rollouts, and reverse any of it with a kill switch that streams to every connected SDK in sub-second, all as configuration rather than a deploy. If you want to run the dark-launch-then-soft-rollout loop on your own features, [start with the Free plan](https://app.featureflip.io/signup) . --- ## Source: https://featureflip.io/blog/feature-flag-anti-patterns/ # Feature Flag Anti-Patterns: 9 Mistakes Behind Real Outages [Blog](/blog/) # Feature Flag Anti-Patterns: 9 Mistakes Behind Real Outages Nine feature flag anti-patterns behind real outages — Knight Capital's $460M loss, Slack's day-long cascade, Google Cloud's June 2025 crash. Each with a fix. May 6, 2026 · Updated June 21, 2026 · - anti-patterns - fundamentals - incidents Feature flags don’t cause outages. Specific anti-patterns do — and they have names. Reusing a deprecated flag bit cost Knight Capital $460 million in 45 minutes ([Henrico Dolfing case study](https://www.henricodolfing.ch/en/case-study-4-the-440-million-software-error-at-knight-capital/) , 2019). A flag-rollout-triggered performance bug at Slack cascaded into a day of degraded service in May 2020 ([Slack Engineering](https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/) ). A code path that wasn’t behind a flag at all crashed half of Google Cloud globally in June 2025 ([ThousandEyes outage analysis](https://www.thousandeyes.com/blog/google-cloud-outage-analysis-june-12-2025) , 2025). The interesting thing about those three incidents isn’t that flags are dangerous. It’s that the failure modes are recurring, named, and avoidable. This post lists nine of them — what each looks like in code, which public postmortem it shows up in, and the fix. **Key Takeaways** - Most flag-related incidents trace back to a small set of named anti-patterns: reuse, client-side authorization, flag-as-config, coupled toggle points, untested off-state, missing kill switches, nested dependencies, no observability, and “set and forget.” - The flag itself is rarely the cause. The hidden coupling between flag identity and code-path identity is. - A flag check is *display logic*, not security logic. Authorization belongs behind a session-validated server check. - Most release flags should live days to weeks, not months ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-types/release-flags) ). Tag flag *type* at creation; lifecycle follows from type. ## 1. Reusing a deprecated flag Nearly a decade before the incident, Knight Capital deprecated a feature called Power Peg. They marked the flag deprecated, switched users away, and disabled the option in the UI. They didn’t remove the code. Years later, an engineer needed a new flag bit and reused the deprecated Power Peg bit. The deploy missed one of eight servers. When the flag was set in production, the eighth server — still carrying the old binary — ran the dead code path. Knight bought roughly $7 billion of stock it couldn’t pay for, lost about $460 million in 45 minutes, and agreed to be acquired by Getco four months later ([Knight Capital Group](https://en.wikipedia.org/wiki/Knight_Capital_Group) , Wikipedia). The framing most retellings reach for is “flags are dangerous.” The narrower framing is more useful: a deprecated flag whose underlying code path still ships is a future incident waiting for an unrelated change to step on it. Two rules prevent the recurrence: never reuse a flag bit or key for a different feature, and remove the *code path* in the same change that retires the flag — in two PRs, flag check first, dead branch second. The full procedure lives in our [feature flag cleanup playbook](/blog/feature-flag-cleanup/) . ## 2. Using a client-side flag for an authorization decision Client-side flags are tamperable. Any user with browser dev tools can flip them in three seconds. That’s fine when the flag controls *display* — whether a button is rendered, whether a tab is shown — because the server still enforces what the user can actually do. It’s an outage-class problem when the flag is the access control: ```js // Anti-pattern: the API trusts the client's flag. if (flags.canExportAllTenants) { return await api.exportAllTenants(); // server has no auth check } ``` The user opens dev tools, sets `flags.canExportAllTenants = true`, and exfiltrates every tenant’s data. The flag was never the boundary; the API was. The fix is a server-side authorization check that doesn’t care what the client believes: ```js // Server-side route handler. if (!session.hasRole('platform_admin')) { return res.status(403).end(); } return res.json(await exportAllTenants()); ``` The flag stays. It now controls whether the **export button** renders. The authorization check is where it belongs. The principle: a flag is display logic. If toggling it changes who can do what, you have an authorization rule wearing a flag’s clothing. ## 3. Using a flag where an environment variable belongs Database connection strings, API endpoints, region keys, log levels, and most numeric tuning parameters don’t belong in a feature flag service. Flags are for *behavior* changes — turn a feature on for a cohort, gradually expose a new code path, kill it under load. Environment variables are for *configuration* — the static, deploy-time values an app needs at startup. The smell is a “flag” whose variant looks like configuration: ```js // Anti-pattern: flag whose value is configuration. const config = client.evaluate('payments-config', user, { endpoint: 'https://payments-v2.example.com', timeoutMs: 5000, region: 'us-east-1', }); ``` Three things go wrong. The app’s startup now depends on the flag service being reachable. A static value becomes a runtime evaluation, paying network cost on every request. And the flag dashboard becomes a config registry that no one audits. The full distinction lives in our [feature flags vs environment variables](/blog/feature-flags-vs-environment-variables/) post; the short version is that if the variant looks like configuration, it is configuration. ## 4. Coupling the toggle point to the toggle router Martin Fowler’s canonical [feature toggles article](https://martinfowler.com/articles/feature-toggles.html) calls out the distinction: the *toggle point* is where the decision is read; the *toggle router* is the logic that decides. Tangling them is what makes flag removal terrifying years later, because nobody knows what they’re deleting. The anti-pattern looks like this — five files, each calling the SDK directly with the same set of inputs: ```js // In billing.js, checkout.js, signup.js, dashboard.js, mobile.js: if (client.evaluate('billing-v2', { tenantId, region, plan, betaCohort })) { ... } ``` Now imagine deleting `billing-v2`. You have to find every call site, verify each one passes the same evaluation context, and trust that none of them silently differ. Wrap the decision once, behind a name: ```js // One file. One named decision. export function isBillingV2Enabled(user) { return client.evaluate('billing-v2', { tenantId: user.tenantId, region: user.region, plan: user.plan, betaCohort: user.betaCohort, }); } ``` Every other file imports `isBillingV2Enabled(user)`. The decision lives once. Removal is one delete and a search-and-replace, not an archaeological dig. ## 5. Not testing the off state — or the on/off transition The most common silent flag failure is shipping a flag that’s only ever been tested in one state. CI exercises the new code path because that’s what the PR added. The old path stops getting touched. When the flag flips back during an incident — or rolls forward to a cohort that exposes a latent bug — the un-exercised path executes for the first time in months. Slack’s May 12, 2020 incident started exactly this way. Around 8:30am Pacific, a [percentage-based flag rollout](/docs/concepts/rollout-strategies/) exposed a longstanding performance bug. Slack rolled the flag back within minutes, but the morning load spike had already pushed the backend into a cascade through stale HAProxy state and webapp autoscaling — by 4:45pm Pacific, the user-visible outage began ([Slack Engineering](https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/) ). The flag itself wasn’t broken. The code under the flag had never been exercised under production load. Two rules cover the gap: every PR that adds a flag check needs a CI test for both branches, and pre-rollout, the on path needs a load test or a canary that actually pushes traffic through it. Per-environment overrides in your flag platform exist for exactly this; use them. ## 6. No kill switch — or one that’s never been flipped Google Cloud’s June 12, 2025 outage is the inverse case study. On May 29, Google rolled out new quota-policy checking code into Service Control. The code path was dormant — it activated only when a specific kind of policy was published — and the rollout itself wasn’t gated behind a feature flag. On June 12, a policy update introduced unintended blank fields, hit the dormant code, and triggered a null pointer dereference. Service Control crashed in regions worldwide, taking down APIs across Compute Engine, Cloud Storage, BigQuery, IAM, Cloud Run, Vertex AI, and Workspace ([ThousandEyes](https://www.thousandeyes.com/blog/google-cloud-outage-analysis-june-12-2025) , 2025). The team had built a “red-button” kill switch into the new feature. They activated it within 10 minutes of identifying the root cause and finished rolling it out across all regions 40 minutes later ([Google Cloud incident report](https://status.cloud.google.com/incidents/ow5i3PPK96RduMcb1SsW) ). Two lessons sit on top of each other. The kill switch worked — Google’s own postmortem is explicit that without it, recovery would have taken hours longer. And the *initial* deploy should also have been behind a flag, defaulting to off, and exercised in production-like conditions before activation. A kill switch you’ve never flipped isn’t a kill switch — it’s a hope. Mark every kill switch as permanent at creation, assign it an owner, and exercise it in a non-production environment quarterly. If it can’t be flipped without breaking something, it doesn’t actually work. ## 7. Nested flag dependencies and “flag spaghetti” When a flag’s evaluation depends on another flag’s value, debugging an incident becomes a graph-traversal problem. The combinatorics get ugly fast — six nested flags create 64 possible code paths, and most teams have tested fewer than five of them. ```js // Anti-pattern: nested flags. 64 untested combinations live below. if (flags.checkoutV2) { if (flags.applePayEnabled) { return checkoutV2WithApplePay(cart); } else if (flags.expressPaymentBeta) { return checkoutV2Express(cart); } else { return checkoutV2(cart); } } else { return legacyCheckout(cart); } ``` Fold the decision into one named function that returns one variant: ```js // One decision. Switchable. Auditable. function checkoutVariantFor(user) { // platform-side targeting; client just renders the chosen variant return client.evaluate('checkout-variant', user); // returns: 'legacy' | 'v2' | 'v2-apple-pay' | 'v2-express-beta' } switch (checkoutVariantFor(user)) { case 'v2-apple-pay': return checkoutV2WithApplePay(cart); case 'v2-express-beta': return checkoutV2Express(cart); case 'v2': return checkoutV2(cart); default: return legacyCheckout(cart); } ``` The platform decides; the client picks. Targeting moves to a place where it can be audited, and the combinatorial explosion collapses into a single multi-arm experiment. CI typically covers fewer than five combinations regardless of flag count. The rest only execute in production. ## 8. No observability on flag evaluations When a flag is the suspect in an incident, you need to answer four questions in the first ten minutes: - Which flags changed in the last hour? - For the affected user, which variant fired? - What’s the variant rollout percentage right now? - Has variant traffic correlated with the error spike? Without per-flag [evaluation events](/docs/concepts/feature-flags/) and an audit log, every one of those is a guess. Most platforms emit evaluation telemetry; if yours doesn’t, instrument the wrapper that calls the SDK so you can correlate variant against error rate at the request level. The minimum bar is: every flag change is logged with who, when, and what (audit log); every evaluation emits an event with flag key, variant, user identifier, and timestamp; a dashboard somewhere shows variant share by flag. None of that is exotic — it’s the same telemetry you’d want for any production system. Flags are just the system whose changes happen *during* an incident, which is exactly when you need the data the most. Featureflip surfaces both today: an audit log on every flag and per-evaluation events through the standard SDK telemetry path. (We don’t yet ship anomaly detection on variant share — that one is on us.) ## 9. “Set and forget” — treating flags as permanent infrastructure A release flag that lives past its rollout becomes a zombie. Once a feature is at 100%, the flag is no longer protecting anything — it’s just a conditional branch a future engineer has to reason about. LaunchDarkly’s documentation is explicit: “Release flags are temporary. After you verify the new code is stable and roll out the feature to 100% of contexts, you should archive the flag” ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-types/release-flags) ). In practice most teams don’t: Pete Hodgson frames toggles as “inventory which comes with a carrying cost” that has to be actively kept low ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). The fix isn’t a quarterly cleanup sprint. It’s a tag at creation. Every flag belongs to one of four lifecycle types, and the lifecycle drives the expected lifespan: Tag flag type at creation. The type determines expected lifespan, and the lifespan determines when triage should fire. The cleanup happens at creation, not at quarter-end. If a flag is [tagged “release” the day it’s created](/docs/guides/creating-flags/) , its expiration date is set then too; the dashboard surfaces it for review the moment it expires. If a flag is tagged “kill switch,” it has an owner, a test cadence, and never appears on a stale-flag report. The full operational loop — detect, triage, remove, prevent — sits in our [feature flag cleanup playbook](/blog/feature-flag-cleanup/) . ## The shorter version Most flag-related incidents come from a small set of named anti-patterns. Don’t reuse a deprecated flag bit — and when you retire a flag, remove the code in the same change. Don’t put authorization decisions in client-side flags. Don’t store configuration in a flag service. Wrap each flag decision in one named function so removal is mechanical. Test the off state, the on state, and the transition. Build a kill switch and exercise it before you need it. Flatten nested flag combinations into a single named decision. Emit evaluation telemetry so flag-suspected incidents are debuggable. And tag every flag with a lifecycle type at creation, so cleanup follows from policy instead of willpower. The Knight Capital, Slack, and Google Cloud incidents are public reminders that the failure modes are real and the costs are large. Most teams will never run an automated order-routing system or operate one of the world’s three largest cloud platforms. The anti-patterns are the same. ## Frequently asked questions ### What is a feature flag anti-pattern? A feature flag anti-pattern is a recurring misuse of feature flags that causes incidents, technical debt, or operational pain. Common examples include reusing deprecated flag bits, using client-side flags for authorization, treating flags as configuration, coupling toggle decisions to the code that reads them, and leaving release flags in place after rollout. Each has a documented fix. ### Can feature flags cause outages? Flags rarely cause outages directly. The anti-pattern around the flag does. Knight Capital’s $460M loss came from a deprecated flag reused without removing the code path it once gated ([Henrico Dolfing](https://www.henricodolfing.ch/en/case-study-4-the-440-million-software-error-at-knight-capital/) , 2019). Slack’s May 2020 incident began with a flag rollout that exposed a latent performance bug. Google Cloud’s June 2025 outage came from a code path that wasn’t behind a flag at all. ### Should feature flags be used for security or authorization decisions? No. Client-side flags are tamperable — any user can flip them in browser dev tools. Server-side flags are safer but mix authorization logic with display logic, making access rules harder to audit. Authorization belongs in a session-validated server check; flags should control display, not access. If toggling the flag changes who can do what, you have an authorization rule. ### How long should a feature flag live in production? It depends on the flag’s type. Release flags should be retired within weeks of hitting 100%. Experiment flags should live for the duration of the experiment plus a two-week stabilization window. Kill switches and plan-tier entitlements live indefinitely *if* they’re documented as permanent and assigned an owner ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-types/release-flags) ). Tag the type at creation; lifespan follows. ### What’s the difference between a feature flag and an environment variable? Feature flags are for *behavior* changes — turning a feature on for a cohort, gradually exposing a new code path, killing it under load. Environment variables are for *deploy-time configuration* — connection strings, region keys, log levels. Flags evaluate at runtime against user context; env vars are static for the life of the process. The full distinction lives in our [feature flags vs environment variables](/blog/feature-flags-vs-environment-variables/) post. Each anti-pattern here has a prescriptive counterpart. For the eight practices that prevent these failure modes, see [the feature flag best practices pillar](/blog/feature-flag-best-practices/) — the prescriptive companion to this post. Featureflip is built around the failure modes in this post: an audit log on every flag, per-evaluation events through the SDK, a one-click kill switch, and flag-type tags at creation so cleanup follows the lifecycle. If you’d like to see how that plays out in practice, [start with the Free plan](https://app.featureflip.io/signup) . It’s free forever for one project. --- ## Source: https://featureflip.io/blog/feature-flag-best-practices/ # Feature Flag Best Practices: 8 Rules for Production Teams [Blog](/blog/) # Feature Flag Best Practices: 8 Rules for Production Teams Eight feature flag best practices for production teams: lifecycle tagging, flat decisions, both-state testing, progressive rollouts, observability, and cleanup. May 27, 2026 · Updated June 21, 2026 · - best-practices - lifecycle - fundamentals Best practice with feature flags is mostly prevention. The rules that matter aren’t ceremonies — they’re the ones that stop a flag from becoming the suspect in next quarter’s incident review. Most feature flags are never removed; Pete Hodgson’s canonical treatment frames toggles as “inventory which comes with a carrying cost” that teams have to actively keep low ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). The same lack of discipline is what reused a deprecated flag bit at Knight Capital in 2012 and cost the firm $460 million in 45 minutes ([Henrico Dolfing](https://www.henricodolfing.ch/en/case-study-4-the-440-million-software-error-at-knight-capital/) , 2019). Most “feature flag best practices” articles list rules without saying which failure mode each one prevents. This one does. Eight rules, each anchored to a specific way flags hurt teams that skip it, each with a concrete “what good looks like” check. **Key Takeaways** - Tag every flag with one of four lifecycle types at creation (release, experiment, kill switch, entitlement). Lifespan, ownership, and removal procedure all follow from the type. - One toggle point per flag. The decision lives in a single named helper, called from everywhere; otherwise removing the flag becomes a graph-traversal problem. - Test both states and the transition in CI. Slack’s May 2020 cascade started on a flag-on code path that had never run under production load ([Slack Engineering](https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/) ). - Roll out progressively (internal → 1–5% canary → 10–20% beta → 100%), then stabilize at 100% for one to two weeks before removing the flag. - Most flags are never removed. The fix is creation-time discipline, not a quarterly purge. **How to use this guide.** This post is the pillar — short on each rule, deep on the inventory. Where a rule has its own depth post, the link is there. For the operational cleanup playbook, jump to our [feature flag cleanup post](/blog/feature-flag-cleanup/) . For the inverse list, see [feature flag anti-patterns](/blog/feature-flag-anti-patterns/) . For the boundary with configuration, see [feature flags vs environment variables](/blog/feature-flags-vs-environment-variables/) . For structured composition between flags, see [prerequisite flags](/blog/prerequisite-flags/) . For the upstream decision of whether to adopt a flag platform at all, see [build vs buy feature flags](/blog/build-vs-buy-feature-flags/) . ## 1. Tag every flag with a lifecycle type at creation Every flag is one of four types, and the type drives everything that follows. **Release flags** are temporary — they gate a new feature during rollout and should be removed within weeks of hitting 100%. **Experiment flags** live for the experiment window plus a stabilization period and then come out. **Kill switches** are permanent, assigned an owner, and exercised on a schedule. **Entitlements** are permanent and gate behavior by plan tier or tenant. A flag that isn’t tagged is, by default, a zombie waiting to happen. LaunchDarkly’s own documentation is explicit: “Release flags are temporary. After you verify the new code is stable and roll out the feature to 100% of contexts, you should archive the flag” ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-types/release-flags) ). The teams that internalize that don’t enforce it with willpower — they enforce it at creation, when the flag is cheapest to govern. The type is the contract. A release flag that lives past day 30 is overdue; a kill switch that's never been flipped isn't a kill switch. What “good” looks like is concrete. Every flag-creation form requires the type, an owner, and — for non-permanent types — an expected removal date. The dashboard surfaces expired release flags for review the day they expire, not the next time someone runs a cleanup sprint. Kill switches carry a documented test cadence; if they can’t be flipped in a non-production environment without breaking something, they don’t actually work. The depth on the operational loop sits in our [feature flag cleanup playbook](/blog/feature-flag-cleanup/) . Tagging the type at creation is the upstream change that makes the loop cheap to run. ## 2. Adopt a naming convention and enforce it in CI A flag name should encode purpose, scope, and lifecycle at a glance. `release_checkout_v3_2026q3` reads correctly six months later when nobody on the original PR is still on the team. `newCheckout` doesn’t — there’s no type, no scope, no quarter to anchor it. The cost of bad names is most visible at deprecation time, when somebody reuses a flag bit because the original purpose was unreadable from the name. That’s the failure mode that produced the Knight Capital incident. The template most teams converge on is `___`. One good name, one bad: ```text release_checkout_v3_2026q3 ← type + area + feature + quarter newCheckout ← no type, no scope, no lifecycle hint ``` A wiki page documenting the convention is worth almost nothing because nobody reads it at creation time. A regex check in CI is worth a lot, because it fails the PR that introduces a non-conforming name: ```js // .github/workflows/flag-name-check.js — fail CI on non-conforming flag keys. const FLAG_KEY_PATTERN = /^(release|experiment|killswitch|entitlement)_[a-z0-9]+_[a-z0-9-]+_(20\d{2}q[1-4]|[a-z]+)$/; const newFlagKeys = collectFlagKeysFromDiff(); // your platform's API or grep const offenders = newFlagKeys.filter(k => !FLAG_KEY_PATTERN.test(k)); if (offenders.length) { console.error('Non-conforming flag keys:', offenders); process.exit(1); } ``` !FLAG_KEY_PATTERN.test(k));if (offenders.length) { console.error('Non-conforming flag keys:', offenders); process.exit(1);}"> Naming is the cheapest governance you can ship. The full depth (patterns by team size, migration paths off bad names, what to do when the convention itself evolves) lives in the dedicated [feature flag naming conventions guide](/blog/feature-flag-naming-conventions/) ; the [creating flags guide](/docs/guides/creating-flags/) covers the Featureflip-specific knobs. ## 3. Keep flag decisions flat — one toggle point per flag When a flag’s decision logic is scattered across the codebase, removing the flag becomes a graph-traversal problem. Martin Fowler calls out the distinction: the *toggle point* is where the decision is read; the *toggle router* is the logic that decides ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Tangling them is what makes flag cleanup terrifying years later, because nobody knows what they’re deleting. The anti-pattern looks like this — the SDK call repeated across five files, the evaluation context subtly different in each one: ```js // In billing.js, checkout.js, signup.js, dashboard.js, mobile.js: if (client.evaluate('billing-v2', { tenantId, region, plan, betaCohort })) { // ... } ``` Wrap the decision once, behind a name, and import it everywhere: ```js // One file. One named decision. export function isBillingV2Enabled(user) { return client.evaluate('billing-v2', { tenantId: user.tenantId, region: user.region, plan: user.plan, betaCohort: user.betaCohort, }); } ``` Every other file calls `isBillingV2Enabled(user)`. The decision lives once. Removal is one delete and a search-and-replace, not an archaeological dig through five evaluation contexts that drifted apart. The rule extends to flag *composition*. If a section of behavior depends on more than one flag, don’t nest the conditionals — fold them into a single named decision (`getCheckoutVariant(user) → 'legacy' | 'beta' | 'rollout'`) and switch on the result. Where the composition is structural (one flag’s behavior genuinely depends on another being on), use [prerequisite flags](/blog/prerequisite-flags/) — they’re the supported pattern. Where it’s accidental, flatten it. The inverse failure mode, including the combinatorics math, lives in the [anti-patterns post](/blog/feature-flag-anti-patterns/) . ## 4. Test both states — and the transition — in CI A flag with only one tested state is a deploy waiting to surprise you. CI exercises the new code path because that’s what the PR added. The old path stops getting touched. When the flag flips back during an incident — or rolls forward to a cohort that exposes a latent bug — the un-exercised path executes for the first time in months. Slack’s May 12, 2020 incident started exactly this way. Around 8:30am Pacific, a percentage-based flag rollout exposed a longstanding performance bug. Slack rolled the flag back within minutes, but the morning load spike had already pushed the backend into a cascade through stale HAProxy state and webapp autoscaling — by 4:45pm Pacific, the user-visible outage began ([Slack Engineering](https://slack.engineering/a-terrible-horrible-no-good-very-bad-day-at-slack/) ). The flag itself wasn’t broken. The code under the flag had never run under production load. Two rules cover the gap. Every PR that adds a flag check needs a CI test for both branches: ```ts // vitest / jest pattern: parameterize the suite over flag state. describe.each([ ['flag on', true], ['flag off', false], ])('checkout (%s)', (_, billingV2Enabled) => { beforeEach(() => mockFlag('billing-v2', billingV2Enabled)); it('charges the right amount', async () => { const result = await checkout(cart); expect(result.total).toBe(expectedTotal(billingV2Enabled)); }); }); ``` { beforeEach(() => mockFlag('billing-v2', billingV2Enabled)); it('charges the right amount', async () => { const result = await checkout(cart); expect(result.total).toBe(expectedTotal(billingV2Enabled)); });});"> And pre-rollout, the *on* path needs a load test or a canary that actually pushes traffic through it. Per-[environment](/docs/concepts/environments/) overrides on your flag platform exist for exactly this; pin the value in staging so the assertion is deterministic. If you’re only going to do one of the two, do the both-branch CI rule first — it costs nothing and catches the common case. Teams merging several times a day hit a harder version of this, because the number of possible flag states grows with every open flag: [trunk-based development with feature flags](/blog/feature-flags-trunk-based-development/) works through which states are worth running. ## 5. Roll out progressively — internal, canary, beta, GA Progressive delivery rolls a change to expanding rings of users — internal team first, then a low-percentage canary, then beta, then general availability. Each ring is a checkpoint with a gating metric: error rate, p95 latency, the business metric the feature is supposed to move. If a ring fails its check, halt and rollback. The flag is the mechanism; the rings are the discipline. DORA’s continuous delivery capability calls this out directly: flags let teams separate deploy from release, which is the precondition for keeping change failure rate down at high deployment frequency ([DORA](https://dora.dev/capabilities/continuous-delivery/) ). Where the beta ring is an opt-in group rather than a percentage band, [who gets early access](/use-cases/beta-programs/) becomes a targeting rule instead of a dial. Four rings, each with a gating metric. Promotion is mechanical; rollback is one flag flip. The 7–14 day stabilization at 100% matters more than it sounds — it’s the window that catches latent bugs only visible at full traffic, before the flag (and the rollback path) is gone. LaunchDarkly’s lifecycle documentation recommends the same band ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-lifecycle-settings) ). The Featureflip-specific knobs — percentage rollout per environment, cohort targeting, the rollback control — live in [rollout strategies](/docs/concepts/rollout-strategies/) . This rule is one slice of a wider discipline; for the full strategy, including the delegation pillar most teams skip, see [progressive delivery, explained](/blog/progressive-delivery-explained/) . ## 6. Treat flag changes as production changes A flag flip in production is a production change. Audit who flipped what, gate prod-flag-edit permissions with role-based access, and require a second pair of eyes on high-blast-radius flags. The [audit log](/product/features/audit-log/) isn’t compliance paperwork — it’s the first artifact incident responders reach for when a flag is the suspect. What “good” looks like, concretely: - Production flag edits gated by an SSO-backed role separate from dev and staging. - Every flag change emits an event with actor, timestamp, before/after values, and an optional reason. - High-risk flags (auth, billing, anything touching more than half of users) require a second approver. - The audit log is retained for at least 90 days and queryable by both flag key and actor. A flag-platform admin role that lets anyone on the engineering team flip any flag in production is the default that bites teams quickest. Split the permission early — read-everything is a different role to write-anything, and write-staging is a different role to write-production. The cost of getting this wrong shows up at incident-time, when “who turned that on?” returns a shrug. Featureflip ships the audit log, RBAC, and per-evaluation events that the rules above lean on; the [team management guide](/docs/guides/team-management/) covers the roles, and on the Business plan you can [sign people in through your own identity provider](/docs/guides/single-sign-on/) and give new arrivals a default role. A built-in second-approver workflow on high-risk flags isn’t in the product today — that’s on us. Until it ships, the lightweight pattern is to gate prod edits behind a peer-pair convention enforced by humans, not the platform. ## 7. Keep evaluation local and observable Flag evaluation should be sub-millisecond and observable. Sub-millisecond means in-process — the SDK bootstraps with a cached config and streams updates, so per-request evaluation pays no network cost. Observable means every evaluation emits an event with the flag key, the chosen variant, the reason (`rule-match`, `fallthrough`, `prerequisite-failed`), and a user identifier you can join with your error logs. The combination is what makes flag-suspected incidents debuggable in minutes instead of hours. If the flag service is unreachable, a local-evaluation SDK should serve the last-known-good config — the app keeps working. If it’s only reachable, the app’s startup latency now includes the flag service’s tail latency, and the flag service is a hard dependency you didn’t ask for. OpenFeature codifies the local-evaluation pattern as the cross-vendor standard for exactly this reason ([openfeature.dev](https://openfeature.dev/) ). When a flag is the suspect in an incident, there are four questions to answer in the first ten minutes: - Which flags changed in the last hour? - For the affected user, which variant fired? - What’s the current variant rollout percentage? - Has the variant cohort’s error rate diverged from baseline? Without per-flag [evaluation events](/docs/concepts/feature-flags/) and an audit log, every one of those is a guess. With them, all four are dashboard queries. The minimum bar is an audit log of config changes; the good bar is per-evaluation events you can correlate with request-level error spikes. None of that is exotic — it’s the same telemetry you’d want for any production system. The difference is that flag changes happen *during* incidents, which is exactly when the data has to already exist. Local-evaluation support across all 11 of our [SDKs](/docs/sdks/overview/) is built around this. ## 8. Schedule cleanup as recurring discipline, not a sprint Flag debt accumulates in days but pays down in quarters. Without a recurring cleanup checkpoint, the stale share of your flag inventory climbs steadily, because nothing in the toolchain fails when a flag goes stale. The fix isn’t a heroic quarterly sprint — it’s a small, scheduled checkpoint (monthly for high-velocity teams, quarterly for everyone else) that runs an automated staleness scan and produces a removal queue. The actual removal procedure is mechanical when the upstream practices are in place: delete the flag check first (the platform now serves the new default), verify in production for a release cycle, then delete the dead branch. Two PRs, in that order. The four-step operational loop — detect, triage, remove, prevent — and the grep / AST scripts that drive it sit in the [feature flag cleanup playbook](/blog/feature-flag-cleanup/) . That’s the depth post. The pillar’s job is to put cleanup at the end on purpose: rules 1 through 7 are the practices that minimize the amount of cleanup you’ll ever have to do. Without them, cleanup is heroic. With them, it’s a 30-minute task on a recurring calendar invite. ## The shorter version Most flag-related problems come from a small set of things people skip at creation time. Tag every flag with one of four lifecycle types — release, experiment, kill switch, or entitlement — and let ownership and removal follow from the type. Name flags so the type, area, and quarter are readable six months later; enforce the convention in CI, not on a wiki. Wrap each flag decision in one named helper so the toggle point and the toggle router stay separable. Test both states and the transition before the flag ships. Roll out progressively through internal, canary, beta, and GA, with a gating metric on each ring. Treat flag flips in production as production changes — audit log, RBAC, second approver for the risky ones. Keep evaluation local and emit a per-evaluation event so incident triage isn’t guesswork. And schedule a small recurring cleanup checkpoint instead of letting debt accumulate to the point of needing a sprint to pay it down. ## Frequently asked questions ### What are feature flag best practices? The eight rules production teams converge on: tag every flag with a lifecycle type at creation, name flags with an enforced convention, keep each flag’s decision in one named helper, test both states in CI, roll out progressively through internal-canary-beta-GA, treat flag changes as production changes, keep evaluation local and observable, and schedule cleanup as a recurring checkpoint instead of a quarterly sprint. ### How long should a feature flag live in production? It depends on the type. Release flags should be removed within roughly 30 days of hitting 100%. Experiment flags should live for the experiment window plus a two-week stabilization. Kill switches and entitlements are permanent *if* they’re documented as such and assigned an owner ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-types/release-flags) ). Tag the type at creation; the lifespan follows. ### How do you manage feature flag debt? By preventing it at creation, not cleaning it up at quarter-end. Most feature flags are never removed; toggles are “inventory which comes with a carrying cost” ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Tag every flag with a lifecycle type, set an expiration date for non-permanent ones, and run a small recurring cleanup checkpoint to clear the queue. The full four-step loop — detect, triage, remove, prevent — lives in our [cleanup playbook](/blog/feature-flag-cleanup/) . ### What’s the difference between a feature flag and a kill switch? A feature flag is the *mechanism* — a runtime branch the platform can flip without redeploying. A kill switch is one *type* of feature flag: permanent, owned, exercised periodically, used to disable a misbehaving feature in production. All kill switches are flags; not all flags are kill switches. Tagging the type at creation makes the distinction enforceable instead of cultural. ### How do you name feature flags? The template most teams converge on is `___` — for example, `release_checkout_v3_2026q3`. Type tells you the lifecycle, area tells you which subsystem, feature is the specific change, and the quarter or owner anchors it in time. Enforce the convention with a regex check in CI; a wiki page never gets read at creation time. ### How do you roll out a feature flag safely? Through four rings: internal team and dogfood first (24–48 hours), canary at 1–5% of production (30–60 minutes per increment, gated on error rate and p95 latency), beta at 10–20% (24–72 hours, gated on the business metric), then general availability at 100%. Stabilize at 100% for 7–14 days before removing the flag ([LaunchDarkly](https://launchdarkly.com/docs/home/flag-lifecycle-settings) ). Each promotion should be an automated rule, not a Slack message. Featureflip is built around the practices in this post: sub-millisecond local evaluation, per-evaluation events through the SDK, an audit log on every flag, RBAC for production edits, and flag-type tagging at creation so cleanup follows the lifecycle. If you’d like to see how that plays out in practice, [start with the Free plan](https://app.featureflip.io/signup) . It’s free forever for one project. --- ## Source: https://featureflip.io/blog/feature-flag-cleanup-tools-compared/ # Feature Flag Cleanup Tools Compared (2026) [Blog](/blog/) # Feature Flag Cleanup Tools Compared (2026) Feature flag cleanup tools split three ways: mark the flag, let a model rewrite the code, or apply deterministic transforms. Which one fits your codebase. September 17, 2026 · - cleanup - debt - automation Search for a feature flag cleanup tool and you get a list of products that do three genuinely different jobs. One tells you a flag is dead and stops there. The next hands your code to a language model and asks it to delete the flag, and the third parses the source and applies rewrite rules to it. All three get filed under the same phrase, and picking the wrong one wastes a quarter. Automated removal itself is no longer unusual. LaunchDarkly, Datadog, Harness and Reflag all ship something that opens a pull request against your repository. The interesting question moved on with them. What matters now is what produces the diff, which languages the rewrite actually covers, and what ends up holding write access to your repo. Everything below was read from each vendor’s own documentation on September 17, 2026. Product pages in this category change fast, so check the primary source before you sign anything. ## 1. Three things get called a cleanup tool The first split is how far the tool goes. Every product in this category automates the first column. What separates them is the middle one, and that is the column nobody puts in a comparison table. **Detect and mark.** Unleash marks a flag potentially stale once it passes its expected lifetime, and its documentation is candid about where that ends: it points at integrations that can open pull requests to remove stale flags, without shipping one itself. Vercel puts an Unreferenced badge on flags its discovery endpoint cannot find in your latest production deployment, and its own cleanup guide is a manual procedure that starts with `rg "old-onboarding-flow"`. Most platforms live here. **Model-written removal.** Reflag’s bot refactors with an LLM and opens a pull request. LaunchDarkly’s Vega is an AI agent that does the same on a 1, 7 or 30-day schedule. Harness ships pipeline templates that call a cleanup agent. Datadog’s stale-flag banner carries a Generate Removal PR with Bits button, which opens a pull request removing the flag-gating logic from your application code. **Deterministic rewriting.** The source gets parsed into a syntax tree and rewrite rules are applied to it. Two products do this, [ours](/docs/integrations/flag-cleanup-action/) and FlagShark’s. Being deterministic does not on its own separate one from the other. What does is which languages the rewrite actually reaches and where the tool ends up running, which is what the rest of this covers. ## 2. What changes when a model writes the diff Run a rule-based rewriter twice on the same commit and you get the same diff. Come back in November, after four months of releases, and that same commit still produces it. That property sounds boring until you are the reviewer, because it means a rewrite you approved once cannot come back different, and a run you audit is the run everyone gets. A model does not offer that. The same flag, the same file and the same prompt can produce a diff that differs between runs, and it certainly differs between model versions you do not control. LaunchDarkly’s documentation makes the shape of this explicit without meaning to: Vega reads instructions from an `AGENTS.md` or `CLAUDE.md` file in your repository to improve its results. That is prompt engineering as a configuration surface, which is a reasonable design and a very different thing from a rule set. A model can also do something the rules cannot. It reads your own `useFlag("checkout-v2")` wrapper, works out what the thing does, and rewrites through it. Rule-based matching keys on callee name plus key string, so an unrecognized wrapper is invisible and the run reports nothing to change. That is real reach a rule set does not have, and if your codebase never calls the SDK directly it may be the reach that matters. You give up knowing in advance. A deterministic tool can publish the list of shapes it declines, because the list is the rules, and [we publish ours](/blog/automated-feature-flag-removal/) . A model’s refusals are discovered one run at a time. One practical wrinkle sits underneath Harness: the cleanup agent requires an Anthropic API key, supplied by you. The model bill is yours, and so is the rate limit. ## 3. Ask which languages the rewrite covers, not the scan This is the question that separates a demo from a deployment, and almost nobody asks it. Finding a dead flag in a Kotlin file is easy, because you are looking for a string. Rewriting that file is a different job. It needs a grammar, a parser and a rule set per language. So a product can honestly advertise detection across a dozen or more languages while its rewriter reaches eight of them, and the two numbers appear on different pages. FlagShark is the clearest published example, and it is transparent about both halves if you read both: the homepage describes AST-based scanning for TypeScript, JavaScript, Go, Python, Java, C#, PHP and Rust with precise regex for the other five, while the cleanup-PR documentation gives a different eight for removal, swapping C#, PHP and Rust out for Kotlin, Swift and Ruby. Reflag documents its own boundary plainly. The clean-up bot removes `isEnabled` and leaves `track`, `config` and `requestFeedback` alone, it keeps the codepath that grants access instead of evaluating which branch survives, and it works best with the React SDK while the capability is in beta. LaunchDarkly and Datadog publish no language list for the removal step at all, so that is a question for your account team, not something to infer from their SDK matrix. For what it is worth on our side, the same thirteen languages that get scanned get rewritten, ERB templates included, because both steps run through the same registry. Rails view code is the one that surprises people, and it is where a fair amount of flag logic actually lives. ## 4. Where it runs, and what holds the write bit A tool that opens a pull request has to be able to push a branch. Where that permission lives differs more than the feature comparison suggests. Four of the five products above are vendor-hosted. Datadog’s runs through its GitHub App with read and write on repository contents and pull requests. Vega installs a GitHub App at the organization level and additionally authenticates your personal GitHub account so it can act on your behalf, works only with private repositories, supports GitHub alone for pull requests, and executes scheduled runs using a stored member’s permissions. Reflag connects at the organization level and its bot submits the pull request. Harness is the exception among the hosted products. Its templates run in your own pipeline, with a Git connector and repository token you supply, alongside the Anthropic key from section 2. Ours runs as a step in your workflow, so the credential that pushes the branch is the `GITHUB_TOKEN` your workflow already carries, with `contents: write` and `pull-requests: write`. The parsing happens in the container on your runner. There is a fuller answer about [what a cleanup run can reach](/questions/is-automated-flag-removal-safe-to-let-into-my-repo/) if that is the part your security review will ask about. Plenty of teams will install the app and be right to. It is a procurement question with a real answer, and some organizations cannot grant repository write to an outside app no matter how good the feature is. ## 5. Which shape fits which codebase Four situations decide this, and a feature table rarely does. **Your code never calls the SDK directly.** If every read goes through a house wrapper, a rule-based tool needs that wrapper named before it can do anything, and until you name it the run is silent. Naming it takes one line of config. If the wrapper layer is genuinely irregular, with reads behind three different helpers and a registry, the model-based tools handle that better today. **You ship a mobile app, or a polyglot monorepo.** Removal coverage binds here, not detection coverage. Swift, Kotlin and Dart are where the published lists thin out, so check the rewriter’s list against the languages your flags actually live in before anything else. **You already run the platform.** If your flags are in Datadog or LaunchDarkly, the first-party tool is the lowest-friction thing available and the integration cost is close to zero. That advantage is real, and no argument about determinism erases it. Weigh it against the reproducibility question and whether their rewriter covers your languages. **You have eleven dead flags and a quiet week.** Then you do not need any of this. The [manual cleanup playbook](/blog/feature-flag-cleanup/) covers the procedure, and for a handful of flags a careful afternoon beats evaluating vendors. ## 6. What to check before switching any of them on Five questions, in the order that saves you the most time. Can you see a full run before it writes anything? A dry run that computes every diff and opens nothing is the cheapest possible evaluation, and it shows you fit that no demo will. How many pull requests can one run open? A tool that proposes two hundred branches on the first morning has turned a code problem into a review problem. What happens when it cannot safely rewrite something? Silence and a clean exit are the dangerous answer, because they read exactly like a clean codebase. Who holds write access, and for how long? Section 4 has the shapes, and this is the question your security review will open with. Does the flag itself get retired when the code does? A removal that leaves the flag sitting in the dashboard has moved the debt without paying it. Closing that loop is worth asking about, and it is usually a separate opt-in workflow rather than something on by default. ## The shorter version - Three different products answer to the phrase cleanup tool: mark the flag, have a model rewrite it, or rewrite it with a parser and rules. - Automated removal pull requests are common now. LaunchDarkly, Datadog, Harness and Reflag all ship one, so the capability is a qualifier rather than a differentiator. - A deterministic rewrite gives the same diff for the same input and can publish what it declines. A model reaches shapes rules cannot express and cannot tell you in advance what it will do. - Detection coverage and rewrite coverage are different lists. Ask for the second one. - Four of the five hosted products open pull requests through an app holding repository write. That is a procurement question with a real answer. - Reflag’s cleanup sits on its Pro plan and is in beta. Vega is GitHub-only and private-repo-only. Constraints like these settle evaluations before the feature list is opened. - If you have a dozen dead flags, clean them up by hand and spend the evaluation time elsewhere. ## Frequently asked questions ### What is a feature flag cleanup tool? Any tool that helps retire a flag after its release is finished. In practice the phrase covers three jobs: detecting flags that no longer change behavior, rewriting the source to remove the dead branch, and opening the pull request that carries that change. Most flag platforms automate the first and leave the rest to you. Only a handful do all three. ### Is automated feature flag cleanup safe? It depends on what the tool does when it is unsure. The failure worth worrying about is a rewrite that compiles and still changes behavior, such as folding a mocked flag read so a test quietly stops asserting anything. A tool that stops and names the file is behaving correctly. A tool that finishes silently on a codebase full of flags is telling you less than it appears to. ### Which languages can automated flag removal handle? That depends on the rewriter, not the scanner, and the two lists differ per product. Deterministic tools publish their rewrite languages because the rules are per-language. Model-based tools mostly publish nothing on this, which makes it a question to ask directly rather than an assumption to make. ### Do I need my flag vendor’s own cleanup tool? No, and the tradeoff is straightforward. A first-party tool already knows which of your flags are dead, so setup is trivial. A third-party tool works across vendors, which matters if you are mid-migration, though it cannot archive the flag afterward in a platform it does not control. ### Does removing the code archive the flag? Usually not by default. Code removal and flag retirement are separate systems, and a tool that does the first and not the second leaves a dashboard slowly filling with flags whose code is already gone. Where closing that loop is supported it is normally an opt-in workflow with its own credential, since archiving a flag is a write and scanning for dead ones is not. Featureflip is built by a small team who got tired of flag debt outliving the features that caused it. The cleanup Action is deterministic, covers thirteen languages, and runs as a step in your own CI. [Start free](https://app.featureflip.io/signup) and point it at one repository in dry-run mode. --- ## Source: https://featureflip.io/blog/feature-flag-cleanup/ # Feature Flag Cleanup: A Playbook for Paying Down Flag Debt [Blog](/blog/) # Feature Flag Cleanup: A Playbook for Paying Down Flag Debt Most feature flags are never removed, and one reused bit cost Knight Capital $460M. The four-step playbook to clean up flag debt safely. April 27, 2026 · Updated September 24, 2026 · - cleanup - debt - fundamentals The most expensive feature flag in history was a single bit on one server, repurposed in 2012 because Knight Capital had run out of bits to use. The code path under the deprecated flag had been dead for eight years. Then that one server executed it again, and the company lost $460 million in 45 minutes. Most teams aren’t a brokerage running an automated order-routing system. The underlying problem is universal anyway. Flags are easy to create and awkward to delete, and almost nothing in your toolchain notices when one goes stale. Pete Hodgson’s canonical treatment of feature toggles names the reason: toggles are “inventory which comes with a carrying cost,” and most teams let that inventory grow unchecked ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). **Key Takeaways** - Most feature flags are never removed, and the debt taxes every code review and debugging session that has to trace which paths a flag still gates. - The fix is a four-step loop: **detect, triage, remove, prevent**. A one-off purge buys you about six months. - The two-PR removal pattern (delete the flag check first, dead branch second) is the specific procedure that prevents Knight Capital-class incidents. - Governance at flag *creation* time (naming conventions, owners, expiration dates) does more than any cleanup sprint. - An MCP-aware AI assistant can now run the detect and remove steps for you, calling `find_stale_flags` to build the queue and editing the code branch in the same pass. ## Why feature flag debt accumulates Creating a flag takes seconds. Getting rid of one takes a code review, a deploy, and someone willing to verify that nothing downstream depended on the dead branch. Nothing reminds you. No compiler error fires and no test goes red when a flag goes stale, so flags pile up the way unused CSS classes pile up: silently, and faster than anyone expects. Illustrative. Without a cleanup cadence, active flags accumulate and the stale share climbs. Exact counts vary by team, and the trajectory is the point. The cost compounds quietly. Every stale flag is a conditional branch reviewers have to trace and a path the runbook has to consider during an incident, plus a line of dead code the next engineer has to reason about. None of it shows up as a single billed number, which is exactly why it’s easy to ignore until the inventory is large. To put an illustrative figure on it: if flag friction costs each engineer even two hours a week at a $100/hour loaded rate, a 50-engineer team loses on the order of $500,000 a year. Adjust the inputs to your own team, since the slope matters more than the total. Every deprecated flag is also a future Knight Capital, sitting under code that still runs but is no longer expected to. ## Step 1 — Detect: find every flag and how stale it is Detection has two halves. **Inventory** is the list of flag keys that exist at all. The harder half is **staleness**: when was each one last meaningful? Combine your platform’s [flag list](/docs/concepts/feature-flags/) with a code-reference scan and a `git log` query, and you have a complete picture in under an hour. If that list comes out of a system your team built, this audit is also the first step of [moving off a homegrown flag system](/blog/build-vs-buy-feature-flags/#moving-off-a-flag-system-you-already-built) , since every flag it retires is one you never have to migrate. If you’re on Featureflip, the staleness half is automatic. Every flag in every environment is classified as Active, [Stale, or Dead](/docs/concepts/stale-flags/) from real evaluation traffic, with the reason attached: fully rolled out, rolled back, no traffic, or never used. Filter the flag list by status and you have your removal queue. The inventory half still needs a code search, which the tools below cover. ### Ask your AI assistant to do the cross-reference Detection is a join. Staleness data lives in your flag platform, references live in your repository, and nothing connects the two for you. An AI coding assistant already has both open, which makes this the shortest route through Step 1 if you work with one. Featureflip ships an [MCP server](/docs/integrations/mcp/) (`@featureflip/mcp`) that hands Claude Code, Cursor, Copilot, and other MCP-aware clients a set of flag-management tools. `find_stale_flags` is the one that matters here. Ask for cleanup candidates and the assistant calls it: ```json { "tool": "find_stale_flags", "arguments": { "project": "checkout", "days": 90 } } ``` Back comes every flag that has gone untouched for at least that many days and sits in a terminal state across all of its environments, tagged with which kind: ```json { "project": "checkout", "olderThanDays": 90, "checked": 34, "stale": [ { "key": "checkout-v2", "reason": "enabled-everywhere", "environments": 3 }, { "key": "exp-cart-upsell-v1", "reason": "disabled-everywhere", "environments": 3 } ] } ``` `enabled-everywhere` is the flag that shipped and never got cleaned up. `disabled-everywhere` means nothing is serving it in any environment, though the conditional is very likely still sitting in your code. The tool reads per-environment enabled state, so a rollout that stalled at 40% in production still comes back as `enabled-everywhere`. Check `flag_status` first. It shows the per-environment picture, which is what tells you whether the rollout finished. The assistant has your repository open too, so it can count the code references itself. What comes back is the shortlist: stale in the platform and still referenced in code. None of this needs an assistant. Every one of those tool calls is a plain [Management API](/blog/feature-flag-management-api/) request underneath, so a scheduled job can build the same queue on a cron. The [MCP server walkthrough](/blog/feature-flags-mcp-server/) covers the rest of the tools. The three tools below do the same join by hand. They work against any flag platform, and they’re worth knowing even when an assistant is driving, because they’re what it’s doing on your behalf. ### The grep one-liner For a small codebase, grep gets you most of the way. Adjust the pattern to whatever evaluation API your SDK uses: ```bash grep -rn 'boolVariation\|isFeatureEnabled\|client\.evaluate' src/ \ | awk '{print $2}' | sort -u ``` This produces a rough catalog of every flag key referenced in code. Crude, but it works. As a sanity check against the platform’s flag list, a key that shows up in code and nowhere in the platform is probably dead already, since nothing on the platform side is serving it. Go the other way for your removal candidates: live in the platform, absent from the codebase. ### A small AST-style scanner Once you outgrow grep, a 30-line Python script gives you reference *counts* per flag key. Save the platform’s flag keys to a text file, one per line, and run: ```python #!/usr/bin/env python3 """Count feature flag references across a codebase. Usage: python find-flags.py flags.txt src/""" import re, sys from pathlib import Path flag_keys = Path(sys.argv[1]).read_text().splitlines() root = Path(sys.argv[2]) extensions = {".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".cs", ".java", ".rb"} counts = {k: 0 for k in flag_keys if k.strip()} for path in root.rglob("*"): if not path.is_file() or path.suffix not in extensions: continue try: text = path.read_text(errors="ignore") except Exception: continue for key in counts: counts[key] += len(re.findall(rf'["\']{re.escape(key)}["\']', text)) for key, n in sorted(counts.items(), key=lambda kv: kv[1]): print(f"{n:6d} {key}") ``` Flags with zero hits are obviously dead. A single hit usually means a stale check that was meant to come out when the rollout completed. Watch for one or two hits clustered in a single file, which is the signature of a leftover branch nobody noticed. ### Git archaeology for creation dates Reference counts tell you what’s stale. For how long it has been that way, ask `git log`: ```bash while read -r flag; do date=$(git log --diff-filter=A -S"$flag" --format=%cs --reverse \ | head -1) printf "%s\t%s\n" "${date:-unknown}" "$flag" done < flags.txt | sort ``` `-S` is git’s “pickaxe”: it finds the commit that introduced (or removed) a string. Combined with `--diff-filter=A` and `--reverse`, you get the date the flag key first appeared. Cross-reference that with your platform’s “last evaluated” timestamp, and any flag with creation date older than 90 days *and* zero recent evaluations is a high-confidence cleanup candidate. Code-reference scanners also exist as open-source tools published by some flag platforms, if you’d rather adopt one off the shelf than maintain the script above. Platform-side staleness tooling varies more than most teams expect. ConfigCat ships a zombie flags report keyed off when a flag was last changed or toggled, while Featureflip classifies from live evaluation traffic. If staleness detection is a deciding factor in your platform choice, the [ConfigCat alternative guide](/alternatives/configcat-alternative/) compares the approaches side by side. ## Step 2 — Triage: not every old flag is removable Some flags belong forever, which makes treating “old” as a synonym for “removable” the easy mistake here. Triage means classifying every flag from Step 1 into one of four buckets *before* you touch the code. ### 1. Release flag, fully rolled out The cleanest cleanup target. The [percentage rollout](/docs/concepts/rollout-strategies/) finished weeks or months ago, the new code path has been the only one serving traffic since, and the old branch is dead weight. Remove the flag check, then remove the dead branch, then delete the flag from the platform. ### 2. Experiment flag, experiment ended The experiment has a winner. The losing arm is dead code, and the flag is no longer evaluated meaningfully. Remove both the flag check *and* the losing arm. Leaving the loser in place is how an A/B test silently turns into permanent dead code. ### 3. Permanent flag Kill switches, plan-tier entitlements, regional gates, and per-tenant overrides legitimately stay forever. They’re runtime targeting rules that happen to live in your flag system. Don’t delete them. Each one does need an explicit owner, a `perm-` prefix in the key (so triage doesn’t have to re-evaluate it next quarter), and a sentence of documentation about what it gates. ### 4. Zombie flag The platform shows zero traffic for 90+ days, and yet the code references are still there. This is the dangerous bucket. The code path under the flag is *probably* dead, but it’s been waiting around long enough that nobody currently on the team remembers what it does. Treat zombies like any other production change: confirm with the team that owned the original feature, check error logs for any evidence the path still runs, and only then proceed to removal. The Knight Capital incident is what happens when zombies are left in place. The flag bit they reused had been a zombie for eight years. ## Step 3 — Remove: how to delete a flag without breaking production Removing a flag is a production change. Treat it like one. The removal that bites you is the branch you were sure was dead, still reachable through some code path nobody traced. The procedure that handles this safely has five steps: ### 1. Confirm zero unexpected traffic The flag has been evaluated only at the rolled-out variant for at least 30 days. No edge cases, no weird per-user overrides still firing. If your platform shows surprise evaluations, find out why before continuing. ### 2. One flag per pull request Never bundle flag removals. If something goes wrong, you want a clean revert that affects exactly one feature. ### 3. Split it into two PRs: flag check first, dead branch second This is the single most important rule in flag cleanup, and the one most teams skip. Don’t do this: ```ts // PR that does too much - if (await client.boolVariation('checkout-v2', ctx, false)) { - return renderCheckoutV2(user); - } else { - return renderCheckoutV1(user); - } - function renderCheckoutV1(user) { /* 200 lines of legacy code */ } + return renderCheckoutV2(user); ``` Do this instead. **PR 1**: make the code path unconditional, but leave the dead branch in place. ```ts - if (await client.boolVariation('checkout-v2', ctx, false)) { - return renderCheckoutV2(user); - } else { - return renderCheckoutV1(user); - } + return renderCheckoutV2(user); function renderCheckoutV1(user) { /* still here, unused */ } ``` Deploy. Watch error rates and traffic for 24 hours. If anything was wrong, the revert is a one-line change and the dead-but-present branch is still there to catch any callers you missed. **PR 2**: delete the dead branch. ```ts return renderCheckoutV2(user); - function renderCheckoutV1(user) { /* 200 lines of legacy code */ } ``` If you’re confident after PR 1 that nothing else calls `renderCheckoutV1`, this is safe. Anything that *does* call it will have shown up in your editor’s “find references” before you opened the PR. This two-PR pattern is exactly what would have prevented Knight Capital. The flag itself was fine. The dead branch was still sitting in the deployed binary on one of eight servers when an unrelated change re-activated it. ### 4. Watch error rates and traffic for 24 hours after PR 1 Treat flag removal like any deploy. Most cleanup-related incidents surface within hours, not days. ### 5. Then delete the flag from the platform This is the irreversible step. Do it after both PRs have shipped and the system has been stable for a week. Once the flag is gone from the platform, any rollback path that depended on flipping it is closed. Archive before you delete, if your platform separates the two. Archiving pulls the flag out of the active list but keeps its configuration and audit history, so a surprise in week two costs you a restore rather than a rebuild from scratch. On Featureflip that is `archive_flag` and `restore_flag` over [MCP](/docs/integrations/mcp/) , or the equivalent Management API calls. Hold `delete_flag` back until the flag has been archived and quiet for a while. ## Step 4 — Prevent: governance that stops the cycle If creation policy doesn’t change, cleanup is a treadmill: you’ll be back here in six months with another 200 flags. Three controls do almost all the work, and none of them require buying anything. ### Naming conventions that encode lifecycle A flag named `new_feature` loses its meaning the day after it ships. Compare `release-billing-annual-plans-2026q2`, which tells you the type, the owning team, the feature, and the rough timeline even after every original author has left the company. A reasonable [naming convention](/docs/guides/creating-flags/) : ```plaintext release-billing-annual-plans-2026q2 # rollout, removable exp-onboarding-progress-bar-v2 # experiment, removable perm-killswitch-fraud-detection # permanent, never to be removed perm-entitlement-pro-plan-features # permanent, plan tier ``` The `release-`, `exp-`, and `perm-` prefixes do most of the work. They let triage immediately separate “should be removed” from “intentionally permanent” without context. ### Expiration dates set at creation Every flag platform should have a “scheduled review” or expiration field. If yours doesn’t, a calendar reminder works. The bar is low: any defaulted-to-30-days reminder beats no reminder at all. Google’s internal practice is to expire experiment flags after 30 days unless an engineer explicitly renews them with a justification ([Statsig glossary](https://www.statsig.com/glossary/feature-flag-naming-convention) ). It’s a mechanism worth borrowing at any size. In Featureflip that’s the optional **Expires on** field when you create a flag, or **Set expiry** on one you already have. When the date passes, the flag moves into stale detection and keeps serving exactly what it served before ([how flag expiration dates work](/docs/guides/flag-expiration/) ). ### A recurring cleanup cadence Run a “flag debt day” monthly or quarterly at one engineering hour per person. Yearly purges get postponed until they’re too big to face. Treat it like dependency upgrades, where a quarter of neglect is a chore and two years of it is a project you have to schedule. Illustrative worked example: about 2 hours/week per engineer of flag-related friction at a $100/hour loaded cost (≈$5,200/engineer/year). Read the slope rather than the exact figure. The investment that prevents most of this is genuinely small. An hour at flag creation to set a name, owner, and expiration, then an hour a month to triage. Skipping both, year after year, is what pushes a team toward the higher end of the chart above. ## The Knight Capital lesson In July 2012, Knight Capital deployed new code for its automated equity order-routing system, called SMARS. The deploy missed one of eight production servers. The new code reused a feature flag bit from a feature called Power Peg, which had been deprecated in 2003 and never removed from the binary ([Henrico Dolfing case study](https://www.henricodolfing.ch/en/case-study-4-the-440-million-software-error-at-knight-capital/) , 2019). When the flag was set in production, seven of eight servers ran the new logic. The eighth server, still carrying the old binary, ran Power Peg. Power Peg’s order-fulfillment reporting had been altered after deprecation, so completed orders were never marked as completed, and the system kept sending more. In 45 minutes, Knight bought $7 billion of stock it couldn’t pay for, lost roughly $460 million in the unwind, and was acquired by Getco three months later ([Knight Capital Group](https://en.wikipedia.org/wiki/Knight_Capital_Group) , Wikipedia). The framing most retellings reach for is “feature flags are dangerous.” That’s the wrong takeaway. The flag was perfectly safe in 2003, when the Power Peg feature was live. It became dangerous only because the flag was retired without removing the code path it gated, and stayed dangerous for nine years until something else turned the bit back on. **Deferred cleanup compounds.** Every deprecated flag still in your binary is a future incident waiting for an unrelated change to step on it. That is what the two-PR procedure in Step 3 buys you: the dead branch never outlives the flag it was gated behind. For a deeper view of the lifecycle this fits into, see the broader question of [when feature flags belong in your stack](/blog/feature-flags-vs-environment-variables/) . Flag reuse is also one of the [nine anti-patterns behind real production incidents](/blog/feature-flag-anti-patterns/) . ## Frequently asked questions ### How many feature flags are never removed? There’s no rigorous public statistic, but the consensus among practitioners is that the majority are never removed. The canonical reference treats toggles as inventory to keep deliberately low, because nothing in the toolchain fails when one goes stale ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). The accumulation taxes every review and debugging session that has to reason about which paths are gated. ### How do you find unused feature flags in your code? The quickest route is to ask the AI assistant already open in your editor. Featureflip’s [MCP server](/docs/integrations/mcp/) pulls the stale list with `find_stale_flags` and checks it against your repository in a single pass. By hand it takes three tools. `grep` gives you a quick reference catalog. A small AST-style script (about 30 lines of Python) takes your platform’s flag-key list and counts references per flag. And `git log --diff-filter=A -S` finds when each flag was first introduced, so you can spot keys that have been stale for years. Either way, cross-reference against your platform’s “last evaluated” timestamp. On Featureflip that half is built in: flags are [classified Stale or Dead](/docs/concepts/stale-flags/) from evaluation traffic, so the platform side of the audit starts as a filtered list rather than a spreadsheet. ### Can an AI agent clean up feature flags for me? It can do most of it, with you approving the removals. Featureflip’s [MCP server](/docs/integrations/mcp/) exposes `find_stale_flags` to build the candidate queue, `flag_status` to confirm a flag is finished in every environment, and `archive_flag` and `delete_flag` to retire it once the code is gone. The assistant has your repository open as well, so it can delete the flag check and the dead branch in the same session, which is the part that usually stalls. Keep the two-PR pattern, and keep a human on the confirmation. An agent is good at finding candidates and writing the diff. Deciding whether a code path is genuinely dead is still your call. Destructive tools carry an annotation, so most MCP clients pause and ask before running them. ### How long should a feature flag live? Most rollout flags should be retired within weeks of hitting 100%. Give an experiment flag the duration of the experiment plus a two-week stabilization window. Permanent flags (kill switches, plan-tier entitlements, regional gates) live indefinitely *if* they’re documented as permanent and assigned an owner. Set an expiration or review date at the moment the flag is created. ### Is it safe to just delete an old flag from the platform? Not on its own. Deleting a flag from the platform without first removing the flag check from code is how you produce silent behavior changes. The SDK will start serving the default value, which may or may not match the rolled-out behavior. Delete the flag check from code first (in two PRs, as in Step 3), then delete the flag from the platform once you’ve verified the system is stable. ## The shorter version Cleanup runs as a loop. Detect every flag and how stale it is. Triage into rollout, experiment, permanent, or zombie. Remove the rollout and experiment flags across two PRs, with the flag check going first and the dead branch following once traffic looks clean. Then head off the next round by setting a name, owner, and expiration at creation time. If you work with an AI assistant, hand it the detect step and the code edits in the remove step. The triage judgment stays with you, and so does the sign-off before anything gets deleted. Knight Capital is the worst-case demonstration of why this matters. What you will actually live with is quieter: dozens of flags accumulated over a couple of years, a steady mental tax on every review and debugging session, slower incident response, and a codebase that’s harder to reason about than it should be. Cleanup sits inside the broader practice. Production teams converge on eight rules (type tagging at creation, naming conventions, flat decisions, both-state testing, progressive rollouts, governance, observability, and recurring cleanup), all covered in [the canonical best-practices reference](/blog/feature-flag-best-practices/) . Featureflip is built around this lifecycle. Flag keys, expiry dates and last-evaluated timestamps are first-class in the dashboard, and the Pro plan adds a named owner to every flag, so naming conventions and stale-flag triage become observable rather than tribal knowledge. When a flag passes its expiry date or stops being evaluated, Featureflip emails whoever is responsible for it. You can also [script the cleanup through the Management API](/blog/feature-flag-management-api/) : list the flags, find the ones a completed rollout has retired, and archive them as a scheduled job instead of a manual pass. Or skip the script entirely and [let the AI assistant already in your editor do it](/blog/feature-flags-mcp-server/) , finding the candidates and removing the dead branch in one sitting. To take the removal step off your plate altogether, a CI job can open one pull request per dead flag, which turns this playbook’s Step 3 into a diff review. What such a tool has to [refuse in order to be worth trusting](/blog/automated-feature-flag-removal/) is the more interesting half of that story, and [the products that do this now](/blog/feature-flag-cleanup-tools-compared/) divide along one line: whether a parser or a language model writes the diff. If you’d like to see how that plays out in practice, [start with the Free plan](https://app.featureflip.io/signup) . It’s free forever for one project. --- ## Source: https://featureflip.io/blog/feature-flag-management-api/ # Feature Flag Management API: Automate Flags with REST [Blog](/blog/) # Feature Flag Management API: Automate Flags with REST Create projects, flags, and targeting rules programmatically with the Featureflip feature flag API. Seed flags in CI, sync environments, build internal tools. July 9, 2026 · - api - automation The dashboard is built for a person: click into a project, flip a flag, watch it roll out. That is the right tool when a human is in the loop. It is the wrong tool when the thing that needs to change a flag is a CI job, a deploy script, or an internal admin panel. Those need an API. Featureflip now has one. The **Management API** is a REST interface for configuring everything the dashboard configures: projects, environments, feature flags, variations, targeting rules, segments, and SDK keys. It is the automation counterpart to the dashboard, and it is separate from the high-throughput serving path your application already uses to read flags. This post covers what the split means, how to authenticate, and a request-by-request walkthrough from nothing to a live, targeted flag. By the end you will also understand the guarantees (idempotency, a single error shape, visible rate limits) that make it safe to script against in a loop. **Key Takeaways** - The **Management API** configures Featureflip over REST: projects, environments, flags, variations, targeting, segments, and SDK keys, at base URL `https://api.featureflip.io` with every path under `/api/v1`. - It is a separate surface from the **Evaluation API and SDKs**. Use the Management API to *write* configuration; use an SDK to *read* flags in your app. Configuration is low-frequency and rich; evaluation is high-throughput and stays on the hot path. - Authenticate with a bearer token: a **Personal Access Token** (`ffp_`, acts as you) for scripting, or a **Service Token** (`ffs_`, a machine identity scoped to one organization) for CI and shared automation. - Built for automation: an `Idempotency-Key` on every create so retries never double-create, one frozen JSON error envelope, cursor pagination, `X-RateLimit-*` headers on every response, and hypermedia `_actions` that tell your code which operations the current token may perform. - Resources are addressed by the same **key or slug** you see in the dashboard, so a script reads like the product instead of a wall of opaque IDs. ## 1. Two APIs: configure versus evaluate Featureflip exposes two API surfaces, and knowing which is which saves you from the most common mistake. The **Management API** (`https://api.featureflip.io`, everything under `/api/v1`) is the write plane. It creates and updates the configuration of your flags: the flags themselves, their variations, which environments they are on, and the rules that decide who sees what. It is called by humans-once-removed: a CI pipeline, a migration script, an internal tool. The **Evaluation API and the SDKs** are the read plane. Your application uses them to answer one question, fast: “what value does this flag have for this user, right now?” That path is built to be lightweight. The server SDKs hold your ruleset in memory and evaluate locally in about the time of a map lookup, with no per-request round trip. That is why the two are separate: putting configuration CRUD on the same path as high-frequency evaluation would slow the part that has to stay fast. The rule of thumb: if you are changing what a flag *is*, that is the Management API. If you are reading what a flag *says* for a given user, that is an SDK or the [Evaluation API](/docs/api/overview/) . The Management API and the Evaluation API act on the same flag configuration from opposite sides. One writes it from your pipeline; the other reads it in your application. Keep configuration CRUD off the request hot path by using the right surface for each job. ## 2. Authenticate with a personal or service token Every request carries a bearer token in the `Authorization` header: ```plaintext Authorization: Bearer ``` "> There are two token types, and the difference is identity. A **Personal Access Token** acts as *you*: it carries your identity and your role in every organization you belong to. Reach for one when you are scripting locally or exploring the API. You create it under **Settings → API Tokens → Personal Access Tokens**, and it is prefixed `ffp_`. A **Service Token** is a machine identity scoped to a single organization. It carries an explicit role and, optionally, an allowlist of projects it may touch. It is not tied to a person, so it keeps working after someone leaves the team. That makes it the right token for CI, automation, and shared infrastructure. You create it under **Organization Settings → Service Tokens**, and it is prefixed `ffs_`. Both token types are shown once at creation, so store them somewhere your automation can read and a person cannot. Both tokens act with a role, and roles are hierarchical: | Role | Can do | | --- | --- | | Owner | Everything, including org settings | | Admin | Manage projects, flags, members | | Member | Create and edit flags, targeting, segments | | Viewer | Read-only | A write attempted by a token below the required role returns `403 forbidden`. A service token whose project allowlist excludes the target project returns `404 not_found` for that project, so a scoped token cannot even confirm the existence of projects it is not allowed to see. The full contract is in the [authentication docs](/docs/management-api/authentication/) . ## 3. Create your first flag over HTTP Here is the whole path from nothing to a live, targeted flag. It is six requests. Everything sends and receives JSON, and every request carries the `Authorization` header from the previous section. Start by finding the organization your token can act in. You can also confirm who the token authenticates as with `GET /api/v1/me`. ```plaintext GET /api/v1/orgs ``` Note the `key` of the organization you want. That value is `{org}` in the paths below. Now create a project inside it: ```bash curl -X POST https://api.featureflip.io/api/v1/orgs/{org}/projects \ -H "Authorization: Bearer $FF_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "key": "checkout", "name": "Checkout", "description": "Checkout flow experiments" }' ``` That returns `201 Created` with the project. Add an environment to it: ```plaintext POST /api/v1/orgs/{org}/projects/checkout/environments ``` ```json { "key": "production", "name": "Production" } ``` Then create the flag itself: ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags ``` ```json { "key": "new-checkout", "name": "New checkout", "type": "Boolean", "description": "Rolls out the redesigned checkout", "clientSideVisible": false } ``` The `201 Created` response includes the flag’s generated `on` and `off` variations and an `_actions` block. One detail worth internalizing: a new flag is **off in every environment** until you turn it on. Creating a flag never exposes it to a single user. Enabling it in an environment is a separate, deliberate call: ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags/new-checkout/environments/production/toggle ``` ```json { "enabled": true } ``` The toggle returns `204 No Content`. At this point the flag is on and serving its default variation to everyone in `production`. To serve it to a subset instead, add a targeting rule. This one points at a segment; you can instead pass `conditionGroups` for attribute-based targeting or `rolloutPercentage` for a [percentage rollout](/glossary/percentage-rollout/) : ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags/new-checkout/environments/production/rules ``` ```json { "description": "Beta users", "variationId": "", "userSegmentId": "", "conditionGroups": [] } ``` ", "userSegmentId": "", "conditionGroups": []}"> That is the full lifecycle. The [quickstart](/docs/management-api/quickstart/) walks the same path with every response body shown in full. The same sequence any onboarding runs, expressed as HTTP. Organizations, projects, flags, environments, and segments are addressed by their key or slug; rules and variations by the GUID returned when you create them. ## 4. What you can automate The reason to want any of this is that flag configuration stops living only in a browser tab and starts living in your pipeline, where it can be reviewed, repeated, and version-controlled. Four patterns cover most of what teams reach for. **Seed flags in CI.** When you stand up a new service or a new environment, its flags should arrive with it, not get hand-created afterward from memory. A script that reads a manifest and calls the create endpoints gives every environment the same flag set every time. Because creates accept an idempotency key (next section), you can rerun the seed job safely. **Keep environments in sync.** Promoting a configuration from staging to production by clicking through two dashboards is where drift comes from. Reading the targeting from one environment and writing it to another over the API makes the promotion a repeatable step you can put in a pipeline instead of a checklist. **Automate stale-flag cleanup.** Flags are, as Pete Hodgson put it, [an inventory that carries a cost](https://martinfowler.com/articles/feature-toggles.html) . Left alone, that inventory grows. Listing flags over the API, finding the ones that are archived or safe to remove, and scripting the teardown turns [flag cleanup](/blog/feature-flag-cleanup/) from a quarterly chore into a job. The API also enforces flag creation conventions at the source, which pairs well with the CI checks in the [naming conventions guide](/blog/feature-flag-naming-conventions/) . **Build internal tooling.** A Slack command that flips a flag, a back-office toggle your support team can reach, an approval workflow that gates a rollout: all of them are the same REST surface with a different front end. You are not blocked on the dashboard supporting your exact workflow, because you can build the workflow yourself. Each of these is a scripted [dark launch or soft rollout](/blog/dark-launches-and-soft-rollouts/) waiting to happen, run from where your team already works. ## 5. Built for automation, not just reachable An endpoint you call once by hand and an endpoint you call in a retry loop have different needs. A management API you script against has to behave predictably when a request is retried, when a payload is wrong, and when you are going too fast. Featureflip’s has four properties for exactly this. **Idempotency.** Every create accepts an `Idempotency-Key` header: ```plaintext Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff ``` Send a unique key per logical create. If a request with the same token and key is retried, the original result is replayed instead of creating a duplicate. If a second request with the same key arrives while the first is still in flight, it returns `409 idempotency_key_in_progress`, so overlapping retries run at most once. That is what makes a rerun of a seed job safe. **One frozen error envelope.** Every error, from any endpoint, has the same shape: ```json { "error": "validation_failed", "message": "One or more fields are invalid.", "docs_url": "https://featureflip.io/docs/management-api/errors/validation_failed", "fields": { "key": ["Key must be lowercase kebab-case."] } } ``` `error` is a stable machine-readable code, `message` is for humans, and `docs_url` links to the page for that code. Some errors add fields: `fields` for per-field validation, `retry_after` when you are rate limited, `did_you_mean` when you typo a key or slug, and `next_actions` suggesting follow-up requests. Your error handling reads one shape, not one per endpoint. **Rate limits you can see.** Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`, so a well-behaved client can slow down before it is told to. When you do exceed the limit, you get `429 rate_limited` with a `retry_after` field and a `Retry-After` header telling you exactly how long to back off. **Hypermedia _actions.** Every resource response includes a small block describing which follow-up operations the current token may perform, and why not when it cannot: ```json "_actions": { "update": { "allowed": true }, "delete": { "allowed": false, "reason": "requires Member role" } } ``` An internal tool can read `_actions` to decide which buttons to show for the current token, without hardcoding a copy of the permission model that will drift the first time the roles change. The [conventions doc](/docs/management-api/conventions/) covers all four in detail, and the [errors reference](/docs/management-api/errors/) lists every code. ## 6. Pagination and addressing List endpoints are cursor-paginated. Pass an opaque `cursor` to fetch the next page: ```plaintext GET /api/v1/orgs/{org}/projects/{project}/flags?cursor= ``` "> The response wraps the items with the next cursor, and a `null` cursor means there are no more pages: ```json { "items": [ /* ... */ ], "next_cursor": "eyJvIjoyNX0" } ``` One quirk worth coding for: because cursors are offset-based, the final “next” page can legitimately come back empty, so treat an empty page as the end rather than an error. On addressing, the pattern is consistent: organizations, projects, flags, environments, and segments are addressed by their **key or slug**, the same stable identifier you see in the dashboard. Nested resources without a user-facing key, meaning targeting rules and variations, are addressed by the **GUID** `id` returned when you create or list them. ## 7. When to use the API, the SDK, or the dashboard Three surfaces, three jobs, and the mistake to avoid is using the Management API where an SDK belongs. | Surface | Use it to | Reach for it when | | --- | --- | --- | | Dashboard | Configure flags by hand | A person is making a one-off change | | Management API | Configure flags programmatically | A pipeline, script, or internal tool changes configuration | | SDK / Evaluation API | Read a flag’s value for a user | Your application decides what to serve, on the request path | The Management API is not a flag-reading API. It is not built for the volume or the latency of per-request evaluation, and pointing your application at it to decide what to render would put a network round trip and a rate limit on your hot path. That is the SDK’s job, and the [server SDKs](/docs/sdks/overview/) evaluate locally in memory precisely so that path stays fast. Use the Management API to set the configuration up; use an SDK to read it out. The [feature page](/product/features/api/) has more on the API surface, and the [SDK feature page](/product/features/sdks/) covers the evaluation side. ## The shorter version Featureflip now has a REST Management API at `https://api.featureflip.io`, with every path under `/api/v1`. It is the write plane: it configures projects, environments, flags, variations, targeting, and segments, separate from the SDKs that read flags on your application’s hot path. Authenticate with a bearer token, a Personal Access Token (`ffp_`) for scripting or a Service Token (`ffs_`) for CI and shared automation. Six requests take you from finding your organization to a live, targeted flag, and a new flag stays off until you deliberately enable it. What makes it safe to script is the automation contract: an `Idempotency-Key` on every create so retries never double-create, one frozen error envelope across every endpoint, visible `X-RateLimit-*` headers, and hypermedia `_actions` that report what the current token may do. Use it to seed flags in CI, sync environments, automate cleanup, and build internal tooling. ## Frequently asked questions ### Can I manage feature flags through an API? Yes. The Featureflip Management API is a full REST interface at `https://api.featureflip.io/api/v1` for configuring projects, environments, feature flags, variations, targeting rules, segments, and SDK keys. It does everything the dashboard does, driven by code instead of clicks, so you can seed flags in CI, sync environments, and build internal tooling. The [overview](/docs/management-api/overview/) lists every resource you can manage. ### What is the difference between the Management API and the Evaluation API? The Management API *writes* configuration: it creates and changes flags, targeting, and environments. The Evaluation API and the SDKs *read* flags, answering what value a flag has for a given user. Evaluation is high-throughput and stays on your request path, so the server SDKs evaluate locally in memory with no per-request round trip. Configuration is lower-frequency and richer, which is why the two are separate surfaces. ### How do I authenticate to the Featureflip Management API? Send a bearer token in the `Authorization` header: `Authorization: Bearer `. Use a Personal Access Token (prefixed `ffp_`) for personal scripting, which acts as you and carries your role in every organization you belong to. Use a Service Token (prefixed `ffs_`) for CI and shared automation, which is a machine identity scoped to one organization and keeps working after team changes. Both are created in the dashboard and shown once. ### How do I create a feature flag with the API? Send `POST /api/v1/orgs/{org}/projects/{project}/flags` with a `key`, `name`, and `type` such as `Boolean`. The response returns the flag and its generated variations. A new flag is off in every environment until you enable it with a separate call to that environment’s `/toggle` endpoint, so creating a flag never exposes it to anyone until you deliberately turn it on. ### Does the Management API support idempotent retries? Yes. Every create endpoint accepts an `Idempotency-Key` header. Send a unique key per logical create, and a retried request with the same token and key replays the original result instead of creating a duplicate. If a second request with the same key arrives while the first is still processing, it returns `409 idempotency_key_in_progress`, so overlapping retries execute at most once. This is what makes rerunning a seed or sync job safe. Featureflip is a focused, flat-priced feature flag platform, and the Management API brings its full configuration surface into your pipeline: create flags, targeting, and segments over REST, with idempotent creates, one predictable error shape, and permission-aware responses that make it safe to automate. The same API also backs the [Featureflip MCP server](/blog/feature-flags-mcp-server/) , so AI coding assistants like Claude Code and Cursor can manage your flags without leaving the editor, and the [Terraform provider](/blog/feature-flags-as-code-terraform/) , if you would rather declare flags in `.tf` files and [run them through pull requests and CI](/blog/gitops-feature-flags/) . The [Management API quickstart](/docs/management-api/quickstart/) takes you from a token to a targeted flag in a few requests, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flag-naming-conventions/ # Feature Flag Naming Conventions: A Practical Guide [Blog](/blog/) # Feature Flag Naming Conventions: A Practical Guide Feature flag naming conventions that hold up: the anatomy of a good key, how to enforce it in CI, how to scale it by team size, and how to fix bad names. July 1, 2026 · - naming - governance - fundamentals Six months from now, someone who never wrote the code will open your flag list and try to work out what `newCheckout` does. Is it still rolling out? Was it an experiment that already picked a winner? Is it safe to delete, or is it a permanent gate holding up billing? The name answers none of those questions, so they will ask in Slack, and if nobody remembers, the flag stays. That is how a flag list turns into a graveyard nobody trusts. The name is the one part of a flag that every tool, teammate, and script sees. It shows up in code review diffs, in the dashboard, in audit-log lines, in `grep` output, and in the SDK call itself. Pete Hodgson’s canonical treatment of feature toggles frames the stakes plainly: toggles are “inventory which comes with a carrying cost,” and teams should “keep that inventory as low as possible” ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). A naming convention is the cheapest tool you have for keeping that inventory legible while it exists, and for making it obvious which items are safe to throw away. **Key Takeaways** - A flag key is a record you query, not a label you read: it should be greppable, sortable, and self-describing to someone who has never seen the code. - The convention most teams converge on encodes four things in the key: **type, area, feature, and a date or owner** (`release_checkout_v3_2026q3`). - The type prefix (`release`, `experiment`, `ops`, `perm`) does most of the work, because it tells triage what is removable without any other context. - Enforce the rule with a regex in CI, not a wiki page nobody reads at creation time. The regex is the real spec. - You cannot safely mass-rename live keys. Migrate opportunistically with a two-key cutover; version the convention rather than retro-applying it. ## 1. A flag key is a record you query, not a label you read The mistake behind most bad flag names is treating the key like a variable name: something short and readable for the person writing this one line of code today. But the key outlives that moment. It becomes a row in a database, a string in a `git log -S` search, a filter in the dashboard, and an entry in every audit-log line that records a flip. That means the key has readers who are not you and not now. A good key answers their questions without a lookup: - **A teammate scanning the flag list** wants to know, at a glance, what this gates and whether it is still live. - **An engineer running an incident triage** wants to `grep` the codebase for every flag in one subsystem, which only works if area is encoded in the name. - **A cleanup pass** wants to sort flags by age and type to find removable ones, which only works if type and a date are in the key. - **The next person to reuse a bit** needs the name to scream “this is permanent, do not touch” or “this shipped in 2024, it is dead.” The absence of that signal is the specific failure that took down [Knight Capital](/blog/feature-flag-cleanup/) : a bit whose original purpose was unreadable from the name, reused years later. So the design goal is not “short and pretty.” It is “legible to a stranger, greppable by tooling, sortable by lifecycle.” Everything below follows from that. ## 2. The anatomy of a good flag key The structure most teams settle on packs four questions into one string: ```text release_checkout_v3_2026q3 ``` Each segment answers one of the four things a future reader asks: A structured key answers type, area, feature, and time in one string. Every segment earns its place by serving a future reader or a script. Read left to right, `release_checkout_v3_2026q3` says: this is a **release** flag (so it is meant to be removed once rolled out), it gates the **checkout** subsystem, it is the **v3** iteration, and it landed in **2026 Q3**. A stranger can triage it without asking anyone. Compare that to `newCheckout`, which is stale the day after it ships and carries no lifecycle hint at all. The public reference conventions land in the same place. Statsig’s naming-convention glossary recommends a “prefix or suffix indicating the flag type (e.g., release, experiment, ops, or permission)” plus a feature-area segment ([statsig.com](https://www.statsig.com/glossary/feature-flag-naming-convention) ). The ordering varies (some teams put the type last), but the four ingredients are stable. One product note: in Featureflip the key is auto-generated from the flag’s human name (so “New Checkout Flow” becomes `new-checkout-flow`), and you can edit it before saving. That auto-generated default is a starting point, not the convention. Edit it to the structured form at creation time, because renaming later is expensive (section 7). The [creating-flags guide](/docs/guides/creating-flags/) covers where that field lives in the dashboard. ## 3. Start with the type: four lifecycle prefixes If you adopt only one segment, adopt the type prefix. It is the segment that makes cleanup possible, because it separates “should eventually be deleted” from “intentionally permanent” without anyone re-deriving the flag’s purpose. Four types cover almost every flag: | Prefix | Lifecycle | Removable? | Example | | --- | --- | --- | --- | | release_ | Ships a change behind a percentage rollout, removed once at 100% | Yes, within weeks of full rollout | release_search_ranking_2026q3 | | experiment_ | A/B or multivariate test, removed when the experiment ends | Yes, plus a short stabilization window | experiment_pricing_page_layout_2026q3 | | ops_ | Operational kill switch or circuit breaker, flipped during incidents | No, lives as long as the dependency it guards | ops_killswitch_recommendations | | perm_ | Permanent gate: plan-tier entitlements, regional rules, per-tenant overrides | No, permanent by design | perm_entitlement_pro_plan_exports | The payoff shows up during triage. When you audit the flag list, the two removable types (`release_`, `experiment_`) are your cleanup queue, and the two permanent types (`ops_`, `perm_`) are the flags you deliberately skip. This mirrors the four buckets in the [flag cleanup playbook](/blog/feature-flag-cleanup/) : a good type prefix means triage is a filter, not a meeting. Tagging the type at creation is also the first of the eight rules in the broader [feature flag best practices](/blog/feature-flag-best-practices/) reference. Naming is where that tag becomes durable, because a tag can be edited away but a key that starts with `perm_` announces its intent every time anyone reads it. ## 4. Delimiter, casing, and length: the boring decisions that bite These choices feel trivial, so teams skip agreeing on them, and then every flag list is a mix of `newCheckout`, `new-checkout`, and `new_checkout` that no single `grep` catches. Decide once: - **Pick one delimiter and one case.** Lowercase with a single separator, applied everywhere. Public conventions lean toward “lowercase letters and underscores” ([statsig.com](https://www.statsig.com/glossary/feature-flag-naming-convention) ); Featureflip’s auto-generated keys use kebab-case (`new-checkout-flow`). Either works. What does not work is mixing them, because inconsistent delimiters break greppability, which is half the reason to have a convention. Whatever you choose, encode it in the CI check (section 5) so drift cannot start. - **No spaces, no camelCase, no uppercase.** Keys travel through URLs, environment variables, log labels, and shell scripts. Keep them to `[a-z0-9]` plus your delimiter and you never have to quote or escape them. - **Budget the length.** Four segments of one or two words each is the sweet spot. `release_checkout_v3_2026q3` is long enough to be self-describing and short enough to type. If a key needs five or six segments to be clear, that is usually a sign the area segment is doing two jobs and the subsystem should be split. - **Encode time, not people.** A quarter (`2026q3`) still means something after the author leaves; an owner initial (`_jdoe`) becomes a dead reference the day they change teams. If you want ownership, put it in the flag’s owner field or [tags](/docs/guides/creating-flags/) , not the immutable key. Reserve the last segment for a date you can sort on. None of this is exciting, which is exactly why it needs to be written down as a rule a machine enforces rather than a habit each person half-remembers. ## 5. Enforce the convention in CI, not on a wiki A naming convention documented on a wiki page is worth almost nothing, because nobody reads the wiki at the moment they create a flag. A convention enforced by a check that fails the pull request is worth a lot, because it catches the non-conforming name before it ever reaches production. Statsig’s guidance says the same thing in operational terms: “implement validation,” “automate flag creation with templates that follow the naming convention,” and “conduct code reviews to catch and correct any deviations” ([statsig.com](https://www.statsig.com/glossary/feature-flag-naming-convention) ). The regex is the real specification. Write the convention down as a pattern and the rest is plumbing: ```js // scripts/check-flag-names.js: fail CI on any non-conforming flag key. const FLAG_KEY_PATTERN = /^(release|experiment|ops|perm)_[a-z0-9]+_[a-z0-9-]+_(20\d{2}q[1-4]|[a-z0-9-]+)$/; // Source the keys however you like: a diff scan, a grep for your SDK's // evaluation call, or your platform's flag-list API. const newFlagKeys = collectNewFlagKeys(); const offenders = newFlagKeys.filter((key) => !FLAG_KEY_PATTERN.test(key)); if (offenders.length > 0) { console.error('Non-conforming flag keys:\n ' + offenders.join('\n ')); console.error('\nExpected: ___, e.g. release_checkout_v3_2026q3'); process.exit(1); } ``` !FLAG_KEY_PATTERN.test(key));if (offenders.length > 0) { console.error('Non-conforming flag keys:\n ' + offenders.join('\n ')); console.error('\nExpected: ___, e.g. release_checkout_v3_2026q3'); process.exit(1);}"> Three layers make it stick, cheapest first: - **A pre-commit hook** that runs the regex against keys added in the diff. It gives the author feedback in seconds, before CI even starts. - **The CI check above**, so the rule holds even when someone skips the hook. This is the gate that actually enforces it. - **A creation-time check against your platform’s API.** The strongest version validates the key when the flag is created, not just when code referencing it lands. If your pipeline [creates flags through the Management API](/blog/feature-flag-management-api/) , it can apply the same regex at the moment the key comes into existence, the earliest point enforcement is possible. Featureflip’s [flag list](/docs/concepts/feature-flags/) is also queryable, so a scheduled job can flag any key that does not match the pattern and surface it as [flag debt](/glossary/flag-debt/) before it spreads. Keep the pattern in one file that both the hook and CI import, so the rule has exactly one definition. When the convention changes, you change the regex, and every layer picks it up. ## 6. Naming by team size The right amount of structure scales with how many people share the flag list. Over-engineering a solo project’s keys is as wasteful as under-structuring an org’s. - **Solo or a single small app.** You are the only reader, and you remember context, so two segments are plenty: `release_dark_mode`, `ops_killswitch_email`. Keep the type prefix (future-you still needs to know what is removable), drop the area if there is effectively one area. - **A small team (roughly 2 to 15 engineers).** Now readers exist who did not write the flag, so the area segment starts earning its place: `release_checkout_v3_2026q3`. This is the sweet spot the four-segment template was designed for. - **Multiple teams in one project.** Add a team or domain namespace as the leading segment so ownership is visible and one team can filter to only its flags: `payments_release_checkout_v3_2026q3`. Resist the urge to encode the environment in the key. Environments are a dimension the platform already models (the same key is evaluated per [environment](/docs/concepts/environments/) ); baking `prod` into the name means you cannot promote the same flag across environments without renaming it. The value of the convention grows with the team, because the whole point is legibility to people who were not in the room. This is illustrative, but the shape holds: the more of the four segments a key carries, the more of the flag list a new engineer can classify without asking anyone. Illustrative. Each segment you add turns another slice of the flag list from "ask someone" into "read the name." The exact percentages vary by team; the direction does not. ## 7. Migrating off bad names without breaking production The hardest question about naming conventions is never how to name new flags. It is what to do about the hundreds of badly-named flags you already have. The honest answer is that you cannot safely mass-rename them, and pretending otherwise is how you cause an outage. A flag key is not a label you can edit freely. It is the identifier your code passes to the SDK and the string the platform matches on to serve a variation. Rename the key in the dashboard without changing every call site, and the SDK starts evaluating a key that no longer exists, so it returns the [default value](/glossary/stale-flag/) for every user. That is a silent behavior change across production, which is exactly the class of incident the two-PR discipline in the [cleanup playbook](/blog/feature-flag-cleanup/) exists to prevent. So migrate deliberately, not in a bulk sweep: - **Freeze the convention going forward.** Turn on the CI check from section 5 so no new non-conforming keys are created. The debt stops growing immediately, which is most of the win. - **Rename opportunistically, with a two-key cutover.** When you next touch a badly-named flag, create the new correctly-named key, point the code at it, verify the new key is serving correctly, then retire the old one. It is the same create-new, cut-over, delete-old pattern you would use for any identifier that lives in both code and a data store. Never flip the key out from under running code. - **Never bulk-rename live keys.** A script that renames every key in the platform will orphan every call site at once. If you must normalize a large batch, do it flag by flag through the cutover above, and only for flags worth the effort. Most bad names attached to soon-to-be-removed release flags are cheaper to simply [delete on schedule](/glossary/flag-debt/) than to rename. The pragmatic path is almost always: enforce the convention on new flags, rename the permanent ones you will keep for years, and let the removable ones age out through normal cleanup. You do not need a pristine flag list. You need one where every flag created from today forward is legible, and where the long-lived flags are worth reading. ## 8. When the convention itself changes Conventions evolve. You will add a fifth type, or move the team namespace to the front, or decide quarters should be `2026-q3` instead of `2026q3`. The instinct is to retro-apply the new rule to every existing flag. Resist it, for the same reason as section 7: renaming live keys is a production change, and doing it in bulk for cosmetic consistency is all risk and no reward. Version the convention instead of rewriting history: - **Bump the regex, not the back catalog.** The CI pattern is the current spec. Change it, and every flag created from that point conforms. Older flags are grandfathered. They were valid under the previous rule and they still evaluate correctly. - **Let cleanup do the normalization.** Removable flags carrying the old convention will age out of the system on their own cadence. Within a couple of quarters, most of the live flag list already follows the new rule without a single rename. - **Only hand-migrate the permanent flags that matter.** If a long-lived `perm_` or `ops_` flag genuinely reads wrong under the new convention, migrate that one flag with the two-key cutover. That is a handful of flags, not the whole list. Treat the convention like any other piece of infrastructure config: the current version applies going forward, and you migrate the past only where the cost is justified. ## The shorter version A flag key is a record other people and other tools query, not a label you read once while writing a line of code. Structure it so a stranger can triage it: encode the type, the area, the feature, and a date (`release_checkout_v3_2026q3`). Of the four segments, the type prefix earns its keep first, because it tells cleanup what is removable without anyone re-deriving the flag’s purpose. Pick one delimiter and one case, encode time rather than people, and write the whole rule down as a regex that fails the pull request. A wiki page is a suggestion; a CI check is the convention. Scale the amount of structure to the size of the team, because the payoff is legibility to people who were not in the room. And be realistic about the flags you already have. You cannot safely mass-rename live keys, so freeze the convention on new flags, rename the permanent ones worth keeping, and let the removable ones age out through cleanup. When the convention itself changes, bump the regex and grandfather the past rather than rewriting it. Good naming is not a one-time cleanup. It is a rule you enforce at creation, every time, so the flag list stays worth reading. Naming sits inside the broader lifecycle: tagging types at creation, testing both states, rolling out progressively, and cleaning up on a cadence. For the full set, see the [eight-rule best-practices guide](/blog/feature-flag-best-practices/) and the [anti-patterns that cause real incidents](/blog/feature-flag-anti-patterns/) . ## Frequently asked questions ### What is a good feature flag naming convention? Encode four things in the key: the type (lifecycle), the area (subsystem), the feature (the specific change), and a date so cleanup can sort by age. The template most teams converge on is `___`, for example `release_checkout_v3_2026q3`. Public references land in the same place, recommending a type prefix such as `release`, `experiment`, `ops`, or `permission` plus a feature-area segment ([statsig.com](https://www.statsig.com/glossary/feature-flag-naming-convention) ). The type prefix is the most valuable segment because it tells you what is safe to remove. ### Should feature flag keys use kebab-case or snake_case? Either works. What matters is picking one and applying it everywhere, because inconsistent delimiters break the greppability that is half the reason to have a convention. Some conventions recommend lowercase with underscores; Featureflip auto-generates kebab-case keys from the flag name. Choose one, keep keys to lowercase letters, digits, and your single separator (so they travel safely through URLs, env vars, and shell scripts), and encode the choice in your CI check so drift cannot start. ### How do you enforce a flag naming convention? Write the convention as a regex and run it in continuous integration so a non-conforming key fails the pull request. A wiki page does not work, because nobody reads it at the moment they create a flag. Add a pre-commit hook for fast local feedback, the CI check as the real gate, and ideally a creation-time check against your platform’s flag-list API so keys are validated the moment a flag is created. Keep the pattern in one shared file so the rule has exactly one definition. ### Can you rename a feature flag key safely? Not in bulk. The key is the identifier your code passes to the SDK, so renaming it in the platform without updating every call site makes the SDK evaluate a key that no longer exists and return the default value for every user, silently. Migrate one flag at a time with a two-key cutover: create the new key, point code at it, verify it serves correctly, then retire the old key. For soon-to-be-removed flags it is usually cheaper to delete them on schedule than to rename them. ### Should the environment be part of the flag name? No. The environment is a dimension the platform already models, so the same key is evaluated separately per environment. Baking `prod` or `staging` into the key means you cannot promote the same flag across environments without renaming it, which reintroduces the risky rename problem. Keep the key environment-agnostic and let the platform handle per-environment configuration. Featureflip treats flag keys, owners, types, and last-evaluated timestamps as first-class in the dashboard, so a naming convention becomes something you can query and enforce rather than tribal knowledge on a wiki. If you want to see how that plays out on your own flags, [start with the Free plan](https://app.featureflip.io/signup) . It is free forever for one project. --- ## Source: https://featureflip.io/blog/feature-flag-outage-recovery/ # Feature Flag Outages: How SDKs Recover and Resync [Blog](/blog/) # Feature Flag Outages: How SDKs Recover and Resync A feature flag outage is survivable when SDKs evaluate from cache and fall back to your defaults. The costly failure is an SDK that never reconnects after. July 12, 2026 · Updated September 24, 2026 · - reliability - sdks Every team plans for their feature-flag service going down. Far fewer plan for it coming back. That asymmetry is where the real cost of an outage hides. A well-built feature-flag SDK makes a control-plane outage close to a non-event for reads. It evaluates flags from an in-memory copy of the last configuration it saw, and anything it cannot resolve falls back to the default you passed in code. Your users keep getting served. Surviving the outage is the easy half. The expensive half is recovery. When the service comes back, does your SDK reconnect and pick up where it left off on its own, or does it sit in a failed state until someone restarts the process? On 2026-07-10, a major feature-flag provider, LaunchDarkly, had a platform outage of roughly an hour ([status page](https://status.launchdarkly.com/) , incident `chywwz01ptb0`). Flags mostly kept evaluating during it. For some teams the pain came after, when the service was healthy again but their SDKs were not. This post draws the line between surviving an outage and recovering from one, walks through the three ways an SDK gets stuck, and gives you a four-point checklist for judging any flag SDK’s recovery behavior. Then it shows how Featureflip’s SDKs meet that bar. **Key Takeaways** - A control-plane outage should be a **non-event for reads**. A server SDK evaluates flags from an in-memory cache of the last config it saw, so evaluation keeps working while the service is unreachable, and anything it cannot resolve returns the **fail-safe default** you passed in code. - The real failure is not the outage, it is **recovery**. An SDK that treats a transient error during the outage as fatal stops reconnecting, so the service comes back but the SDK does not, and the only fix is restarting the process fleet-wide. - Resilient recovery is one invariant: **a running SDK must never permanently stop reconnecting, and must re-sync its full state on every recovery, with no operator intervention.** - Four properties deliver it: never give up, treat a mid-outage `401`/`403` as transient rather than fatal, fall back to polling if streaming keeps failing, and re-sync a full snapshot on reconnect so flags changed or deleted during the outage are corrected. - Featureflip’s SDKs are built to that invariant, so a Featureflip outage never requires a customer restart. Reads stay on cached last-known-good, reconnects retry forever with backoff, and every reconnect pulls a full config snapshot. ## 1. Surviving an outage is the easy half With a server SDK, evaluating a flag is a [local, in-memory lookup](/glossary/flag-evaluation/) against a ruleset the SDK already holds. It is not a network call. So when your flag provider’s control plane goes offline, evaluation does not stop: the SDK keeps serving the last configuration it received, and any flag it cannot resolve returns the default you passed at the call site. Two safety nets make that true. The first is the **local cache** of last-known-good config. A server SDK streams configuration in the background and keeps a copy in memory, so reads on the hot path never touch the network and never depend on the service being up at that instant. The second is **fail-safe defaults**: every evaluation takes a default, and a missing flag, a type mismatch, or an evaluation error returns that default rather than throwing. Between the two, a brief outage is usually invisible to your users. This is table stakes, and it is also where most reliability discussions stop. “We serve cached values during an outage” is true of every mature SDK, and it is not the interesting property. Surviving the outage was never the hard part. Coming back is. ## 2. The 2026-07-10 outage, and the part that actually hurt On 2026-07-10, LaunchDarkly had a platform outage of roughly an hour ([status.launchdarkly.com](https://status.launchdarkly.com/) , incident `chywwz01ptb0`). For many teams the survivable half worked as designed: flags kept evaluating on cached values while the service was unreachable, and applications stayed up. The cost for affected teams landed on the other side of the incident. When the service returned, SDKs that had logged an authentication or connection error during the blip did not all reconnect on their own. The service was healthy, but those SDKs were still sitting in a stopped state, and the fix was a manual application restart, done across the fleet. That pattern is not specific to one vendor. It is a **failure class**: any SDK that treats a temporary error during an outage as a terminal condition can land there, and the anti-pattern is common enough that the incident is worth studying rather than dunking on. We are not here to litigate anyone’s internals. The design lesson is what matters, and it applies to every provider, including us. It is the reason teams weighing [Featureflip against LaunchDarkly](/compare/featureflip-vs-launchdarkly/) , or scanning [LaunchDarkly alternatives](/alternatives/launchdarkly-alternative/) , increasingly ask not just “what is your uptime” but “what does your SDK do when the service comes back.” ## 3. Why an SDK gets stuck An SDK fails to recover in one of three ways. All three are the same mistake wearing different clothes: something temporary was treated as permanent. **It gives up on a transient error.** A `401` or `403` received mid-session during an outage is indistinguishable from a real key revocation. It can be an auth-proxy blip, or the configuration service failing its own key validation closed because a dependency is down. An SDK that reacts by stopping has converted a 60-second wobble into a manual restart. The tell is a log line shaped like this: ```text [flags] auth error (401): giving up, will not retry ``` That “will not retry” is the whole bug. The word “permanently” in a client library’s log is almost always an operator’s cue to go restart something. **It bricks on a failed startup.** If the process starts *during* the outage, its first config fetch fails. An SDK that makes a failed initial fetch terminal, rejecting initialization and caching the rejection, serves defaults forever, even after the service recovers, because it never started its background data source at all. Cold-start-during-outage is the nastiest variant, because the SDK looks initialized to your code while being permanently blind. **It reconnects but never re-syncs.** This is the subtle one. The SDK reconnects fine, but only resubscribes to *future* changes. Anything that changed while it was disconnected, or was deleted, stays stale until it happens to change again. The stream shows green and the data is quietly wrong. A [kill switch](/use-cases/kill-switch/) you flipped during the outage might not take effect on a reconnected client that never asked for the current state. All three are recoverable by design. The next section is the design. ## 4. What resilient recovery looks like Resilient recovery is one rule, and everything else follows from it: **a running SDK must never permanently stop trying to reconnect, and must re-sync its full state on every recovery, with no operator intervention.** Four properties deliver that invariant. Together they are a checklist you can hold up against any flag SDK, ours included. - **Never give up.** Reconnection backs off exponentially with jitter, capped at a maximum *interval* (around 30 seconds), never a maximum *retry count*. While the client object is alive there is no terminal state. - **Transient, not fatal.** A `401` or `403` on a client that has run before is treated as a transient auth blip: keep serving last-known-good, log at warning level with a “retrying” framing, and keep retrying. If the key really was revoked, an SDK that harmlessly retries and serves defaults is strictly better than one that goes dark and forces a restart. - **Fall back to polling.** If streaming keeps failing past a threshold, degrade to a periodic full re-fetch, which itself retries forever. A long outage still gets a corrective refresh instead of an endless reconnect loop. - **Re-sync the full state on reconnect.** On every reconnect the SDK pulls a full snapshot of config and applies it as a *replace*, not a merge, so a flag changed or deleted during the disconnect is corrected the moment the stream is back. This is what fixes the silent-staleness failure from the previous section. Both SDKs survive the outage by serving cached values. Only the bottom one recovers on its own: it keeps retrying, then re-syncs a full snapshot the moment the service returns. The top one treated a transient error as terminal and needs a human to restart it. There is one more property, for the startup case. A **non-terminal initialization** closes the cold-start-during-outage hole: a failed first fetch still starts the background data source, serves your defaults meanwhile, and self-heals when the service returns, instead of caching the failure and going blind. ## 5. How Featureflip’s SDKs recover Featureflip’s SDKs are built to that invariant, so a Featureflip control-plane outage never requires a customer restart. Mapping the four properties to what the SDKs actually do: **Reads stay up.** Server SDKs evaluate locally from cached config, so an outage never blocks evaluation, and fail-safe defaults cover anything the SDK cannot resolve. Nothing on your request path waits on the control plane. **Reconnects retry forever.** A dropped stream reconnects with capped exponential backoff and jitter, and if streaming keeps failing the SDK degrades to polling, which also retries indefinitely. There is no give-up state and no log line telling an operator to restart. **A transient 401/403 is treated as transient.** During an outage, an auth error on a client that was previously healthy does not end the client. It keeps serving last-known-good and keeps retrying, on the assumption that a running client hitting a sudden auth failure is far more likely to be seeing a blip than a real revocation. **Every reconnect re-syncs a full snapshot.** On each connect the SDK receives a complete config snapshot and applies it as a replace, so a flag toggled or removed during the outage is correct the instant the stream is back. This holds across the whole server-SDK lineup (JavaScript, Node, Python, Go, Java, C#, and Ruby; PHP polls), and the client SDKs (React, Browser, Swift, Android, and Flutter) get the same full-snapshot treatment on reconnect. **A process that starts mid-outage still comes up.** Initialization is non-terminal: the SDK starts, serves your defaults, and self-heals to live config when the service returns, rather than bricking on the first failed fetch. We did not take this for granted. The 2026-07-10 incident was a useful prompt to re-audit our own SDKs against this exact failure class and confirm they meet the bar, because the “safer than an SDK that needs a restart” claim is only worth making if it is backed by shipped behavior. If you want the mechanics per language, the [SDK overview](/docs/sdks/overview/) is the place to start, and the [SDK feature page](/product/features/sdks/) covers local evaluation, live streaming, and the direct HTTP fallback. ## 6. What resilience does not do Recovery keeps you serving and gets you back to correct state on your own. It is not magic, and three limits are worth stating plainly so you design around them rather than assuming them away. **It cannot serve a flag it has never seen.** A brand-new process that has never once reached the service has only your in-code defaults to serve. That is exactly why the default you pass is your real outage behavior for a cold start: choose it as the value you would be comfortable serving to everyone, because during a cold-start outage that is what happens. Last-known-good requires having once been good. **No new configuration propagates during the outage.** You cannot flip a flag while the control plane is down. Resilience is about not going dark and recovering cleanly, not about pushing config changes mid-outage. The [kill switch](/use-cases/kill-switch/) you rely on works because the last state you set is already cached in every SDK, not because you can reach the dashboard while it is offline. **It is not a substitute for your own monitoring.** An SDK recovering silently is the goal, but your observability should still record that the stream was down and came back. Resilience hides the blast radius from your users. It should not hide the event from your dashboards. ## 7. When the flag change is the incident Sections 1 to 6 assumed the flag service was what broke, but a flag incident can also start with a healthy service and a person. Someone turns on `new-checkout` in production at 14:32, and by 14:33 the checkout error rate is climbing. Rolling back takes one click. Someone has to connect the two first, and the latency graph in front of the on-call engineer says nothing about flags. Put flag changes on your graphs. Featureflip’s [outbound webhooks](/docs/guides/webhooks/) send a signed HTTP request for every change, with the actor, the environment, an `occurredAt` timestamp and a readable description such as `Toggled "new-checkout" on in Production`. Point a subscription at a small handler of your own that checks the signature first. From there it can post the description to your on-call channel and write a Grafana annotation stamped at `occurredAt`, or the equivalent in whatever tool draws your graphs. An error spike starting thirty seconds after a marker that reads `Toggled "new-checkout" on in Production` explains itself, and the fix is the same toggle in reverse. To keep the channel quiet, subscribe only to `flag.toggled`, `flag.environment_config.updated` and the three `targeting_rule` events, limited to the projects you care about. The [audit log](/docs/guides/audit-log/) has the same history for anyone reading it after the fact. ## The shorter version Surviving a feature-flag outage is the easy half: a server SDK evaluates from an in-memory cache and falls back to your defaults, so a brief control-plane outage is usually invisible. Recovering without a restart is the hard half, and it is where the 2026-07-10 LaunchDarkly outage cost some teams the most, when the service returned but their SDKs stayed stopped. An SDK gets stuck three ways: it gives up on a transient error, it bricks on a failed startup, or it reconnects without re-syncing and serves stale data. Resilient recovery is one invariant with four properties: never stop reconnecting, treat a mid-outage auth error as transient, fall back to polling, and re-sync a full snapshot on every reconnect. Featureflip’s SDKs are built to it, so a Featureflip outage never needs a customer restart. ## Frequently asked questions ### What happens to my app when a feature flag service goes down? With a server SDK, evaluation is a local in-memory read from cached config, so flags keep evaluating at their last-known-good values while the service is unreachable, and anything the SDK cannot resolve returns the in-code default you passed. A brief control-plane outage is normally invisible to your users, because reads never depend on the service being reachable at that instant. ### Do feature flags keep working during an outage? Reads do: local evaluation plus fail-safe defaults keep serving. What pauses is config *changes*, because you cannot propagate a new flag value while the control plane is down. The SDK keeps serving the last values it saw and resumes live updates when the service returns. ### Why do some feature flag SDKs need an app restart after an outage? Because they treat something temporary as terminal. A transient `401`/`403` from an auth blip, or a failed initial fetch at startup, makes the SDK stop reconnecting. The service recovers but the SDK does not, so the only fix is restarting the process. A resilient SDK treats those conditions as transient and keeps retrying instead. ### How should a feature flag SDK recover from an outage? It should never stop reconnecting (exponential backoff with a capped interval, never a capped retry count), fall back to polling if streaming keeps failing, and re-sync its full state on every reconnect so flags changed or deleted during the outage are corrected, all with no operator intervention. A non-terminal initialization closes the cold-start case, where the process started while the service was down. ### How do I tell if a feature flag change caused an incident? Put flag changes on the timeline you already use for incidents. Send a webhook for every toggle and targeting change to a handler that posts to your on-call channel and writes a dashboard annotation at the moment of the change. A metric that breaks right after a flag marker points straight at the flag, and switching it back is the first thing to try. ### Is LaunchDarkly reliable? LaunchDarkly is a mature, widely used platform, and like any cloud service it has occasional outages, for example the roughly one-hour incident on 2026-07-10 reported on its status page. For any provider, the reliability question that matters most is recovery: when the service returns, does your SDK reconnect and re-sync on its own, or does it need a fleet restart? Evaluate that behavior for whatever platform you run. Featureflip’s SDKs are built to recover without a restart. Featureflip is a focused, flat-priced feature flag platform, and its SDKs are built so a control-plane outage is a non-event: reads keep evaluating from a local cache, fail-safe defaults cover the rest, reconnects retry until they succeed, and every reconnect pulls a full config snapshot so nothing stays stale, all without a restart. Because it uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations, that local-evaluation model also means an outage or a traffic spike never turns into a surprise bill. The same caching behavior is what decides [whether running the flag service yourself is worth it](/blog/self-hosted-feature-flags/) , since an outage you own and an outage you rent fail in different places. Read the [SDK overview](/docs/sdks/overview/) to see how it works in your language, learn how the [SDKs are built for fail-safe evaluation](/product/features/sdks/) , and [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flag-pricing-explained/ # Feature Flag Flat Pricing vs Per-Seat and Usage-Based [Blog](/blog/) # Feature Flag Flat Pricing vs Per-Seat and Usage-Based Feature flag flat pricing versus per-seat and usage-based: how each model scales as your team and traffic grow, with a real cross-vendor cost comparison. June 23, 2026 · Updated September 14, 2026 · - pricing - comparison Two feature flag tools can advertise almost the same headline number and bill you wildly differently a year later. The reason is the pricing *model*, not the sticker. One tool charges a flat monthly fee. The next charges per engineer who can edit a flag. The third charges per evaluation, per monthly active user, or per API request. Same job, three meters, and the meter you pick decides whether the bill stays still or climbs every time you hire someone or get more traffic. This is a guide to those models. It explains how flat, per-seat, and usage-based feature flag pricing actually work, why per-seat is the one that quietly punishes growing teams, where “free” open source hides its real cost, and what a single mid-size team would pay across six real vendors. Every price below is each vendor’s published list pricing as of September 2026. Pricing pages change, so treat the figures as a snapshot and check the live page before you commit. **Key Takeaways** - Feature flag pricing comes in three meters: **flat** (a fixed monthly fee), **per-seat** (a fee for every editor), and **usage-based** (a fee per evaluation, MAU, or API request). The meter, not the headline price, decides how the bill behaves as you grow. - **Per-seat is the model that scales against you on team size.** At $40 to $75 per seat, a team of 8 pays 8 times the list price, and the bill grows with every hire whether or not the new engineer touches a flag. - **Usage-based pricing tracks traffic, not headcount,** so it starts cheap and gets unpredictable: a successful launch that triples client-side traffic also triples the flag bill. - **Flat pricing is the only model where the plan number is the bill.** It is the wedge Featureflip is built on: $0 / $49 / $99 / $249 per month, with nothing metered: no seats, MAU, or evaluations. Growing your team costs nothing extra until you outgrow a tier’s cap. - “Free” open source is a real option, but the license fee is the small number. You pay in hosting, upgrades, and on-call instead. ## 1. The three ways feature flag tools charge you Strip away the marketing and every feature flag tool meters you one of three ways, sometimes two at once. **Flat (subscription).** You pay a fixed monthly or annual fee for a tier. The tier caps something (projects, flags, environments, retention), but within those caps the number on the invoice does not move. Featureflip and ConfigCat sit here. Adding engineers or traffic does not change the bill until you outgrow a capacity limit and step up a tier. **Per-seat.** You pay for every user, or every *editor*, who can touch a flag. The headline looks small ($40 to $75 per seat per month is the going rate), but the invoice is headcount times that number. GrowthBook Cloud, Unleash, and Flagsmith all meter seats in some form. This is the model that turns a hiring win into a line-item increase. **Usage-based.** You pay for what the system does: evaluations, monthly active users (MAU), or API requests. LaunchDarkly bills per service connection plus per client-side MAU; PostHog bills per flag request. The bill is decoupled from your team and coupled to your traffic, which is great at zero and tense at scale. A fourth path, open-source self-hosting, looks free because the license fee is zero. It is a real choice, covered in section 6, but the cost moves from an invoice to your infrastructure and your on-call rotation rather than disappearing. The rest of this guide takes the three metered models one at a time, then runs one team’s numbers through all of them. ## 2. Per-seat pricing: the tax on adding engineers Per-seat is the model most likely to surprise you, because the unit price is designed to look reasonable in isolation. Forty dollars a seat sounds fine. The catch is that the thing being multiplied (your team) is the thing you are actively trying to grow. Walk a single team up the seat count. GrowthBook Cloud Pro is $40 per user per month. One user is $40. Five is $200. Ten is $400. Twenty-five is $1,000 a month, $12,000 a year, for the same product the first editor used. Unleash’s cloud Pay-As-You-Go plan is steeper at $75 per seat per month, so eight engineers is $600 a month and twenty-five is $1,875 a month, $22,500 a year at list. Flagsmith bundles a few seats into each tier and then charges roughly $50 to $60 for each additional seat, which produces the same upward slope once your team passes the bundled count. None of these is hidden. They are all on the pricing pages. The point is that the slope is the product of a number you control downward (price per seat) and a number you are pushing upward (engineers), and the second number wins over time. Put your own headcount through the [GrowthBook cost calculator](/tools/growthbook-cost-calculator/) and the slope stops being abstract. List prices, September 2026. The flat line barely moves as the team grows; the per-seat lines fan upward because the bill is headcount times seat price. There is a softer version of the problem too. Per-seat pricing pushes teams to ration access, because every additional login costs money. So the product manager who wants to flip a flag during a launch, or the support lead who needs to read targeting rules, gets left off the license to control spend. Flags work best when the people closest to a release can operate them, and a meter that taxes each of those people works against that. The usual defense of per-seat pricing is that read-only viewers should ride free. GrowthBook’s own FAQ rules that out: a seat equals a user, and every login needs one. And for the engineers who ship behind flags, the seat is unavoidable, and that is the population that grows. ## 3. Usage-based pricing: the bill that moves with your traffic Usage-based pricing swaps the headcount meter for a traffic meter. Instead of paying per engineer, you pay per unit of work the flag system does: an evaluation, a monthly active user, or an API request. It is the model with the friendliest on-ramp and the least predictable cruise. LaunchDarkly’s Foundation plan is the textbook case. It includes the first 5 service connections each month, then bills $12 per additional connection plus $10 per 1,000 client-side monthly active users on month-to-month billing (annual billing is lower, about $10 and $8.33 per unit). PostHog meters flags by the request: the first million requests a month are free, then it is $0.0001 per request from one to two million, sliding down to $0.00001 per request above fifty million, a curve the [Featureflip and PostHog cost comparison](/compare/featureflip-vs-posthog/) walks through against a flat plan. Both pricing pages reward you for being small. A side project pays nothing. A pre-launch app pays close to nothing. The meter only becomes a budgeting problem when the product works, which is the point at which teams tend to start weighing the [flat-rate alternatives to LaunchDarkly](/alternatives/launchdarkly-alternative/) . That is the structural tension. The success case and the expensive case are the same event. A feature you launch behind a flag, that then drives a traffic spike, raises the flag bill at exactly the moment the flag did its job. With per-seat you can at least forecast cost from a hiring plan. With usage-based the input is your own growth curve, which is the number you are least able to predict and least want to suppress. Usage pricing also tends to interact badly with client-side evaluation, where every browser or device that checks a flag is a billable event; server-side local evaluation, where flags are checked in-process against a cached ruleset, usually sidesteps per-check billing, which is why the same vendor can be cheap for one architecture and expensive for another. Usage-based is not a trap, to be clear. For a small workload it can be the cheapest option on the board, sometimes literally free. It just trades a predictable bill for a cheap-when-small one, and you should know which of those two properties you are actually buying. ## 4. Which feature flag tools use usage-based pricing Four of the major feature flag tools meter usage in some form: **LaunchDarkly**, **PostHog**, **Flagsmith**, and **Unleash**. The important thing the label hides is that they do not meter the *same unit*, so “usage-based” tells you almost nothing on its own. One tool counts the browsers that check a flag, another counts the backend services that connect, another counts raw API calls. Two tools can both be usage-priced and produce bills that behave nothing alike for the same application. Here is what each one actually counts, with the free allowance and the rate once you pass it. Figures are list pricing as of September 2026, taken from each vendor’s pricing page via our comparison pages, which we re-verify quarterly. | Tool | What the meter counts | Free allowance | Rate above the allowance | | --- | --- | --- | --- | | LaunchDarkly | Two meters at once: server-side service connections and client-side MAU | Developer plan: 5 service connections and 1,000 client-side MAU | Foundation: first 5 connections included, then $10 per connection and $8.33 per 1,000 client MAU on annual billing ($12 and $10 month to month) | | PostHog | Flag requests | First 1M flag requests a month | $0.0001 per request from 1M to 2M, sliding down to $0.00001 above 50M | | Flagsmith | API requests, bundled into tiers | Free plan: 50,000 API requests a month, 1 seat | Start-Up $45/mo includes 1M, Scale-Up $300/mo includes 5M, then from $50 per additional million, falling with volume | | Unleash | Seats, with an API traffic ceiling on top | None. No free hosted tier | $75 per seat a month, with 53M cloud requests a month included, then $5 per additional million | The other three tools in this comparison price by seat or by flat tier. [GrowthBook](/compare/featureflip-vs-growthbook/) Cloud is per seat ($40 a user, free up to 3 users), with overage only past its CDN and warehouse allowances on Pro. [ConfigCat](/compare/featureflip-vs-configcat/) and Featureflip are flat, with capacity ceilings rather than meters, and ConfigCat sells add-on blocks when you run past its traffic allowance. Featureflip runs unlimited evaluations on every plan including the free one, which is the whole point of the flat model covered in the next section. Notice that “usage-based” stretches across four genuinely different mechanics. PostHog is the pure case: one meter, one unit, no seat component. LaunchDarkly runs two meters simultaneously, so a team with many backend services and few browser users lands somewhere completely different from a consumer app with the reverse shape. Flagsmith is really tiered pre-purchase with an overage rate, so you buy a block of requests and pay for spillover. Unleash is per-seat first with a usage ceiling bolted on, which means it can bill you on both axes at once. Only one of those four is what most people picture when they hear “pay for what you use.” So the useful question is not whether a tool is usage-priced. It is three narrower ones. **What unit does the meter count**, and is it something your architecture produces a lot of? **Does client-side evaluation multiply it**, since every browser or device that checks a flag is typically its own billable event while server-side local evaluation against a cached ruleset usually is not? And **what happens at the ceiling**: an overage rate that keeps working and keeps charging, or a hard cap that stops serving? Those three answers predict your bill far better than the pricing-page headline does. ## 5. Feature flag flat pricing: the plan number is the bill Flat pricing is the model where the invoice holds still. You choose a tier, the tier sets some capacity ceilings, and inside those ceilings nothing you do to your team size or your traffic changes the number. There is no per-seat multiplier and no per-evaluation meter, so the only way the bill moves is when you deliberately step up a tier for more capacity. This is the [feature flag flat pricing](/pricing/) wedge, and it is a real gap in the market: flat, predictable billing is the thing growing teams want, and the big platforms mostly do not offer it because per-seat and per-usage meters earn them more as you scale. Featureflip is built on this model: the Free plan is $0 a month forever, Team is $49 a month ($39 billed annually), Pro is $99 a month ($79 annually), and Business is $249 a month ($199 annually). Nothing is metered: no seat fees, no per-MAU fees, and unlimited evaluations on every plan. A traffic spike costs nothing extra, and the bill only moves when you outgrow a tier’s team or project ceiling. The plan number is the bill. ConfigCat is the other major vendor in the flat camp, and including it keeps this honest: flat pricing is not unique to one tool. ConfigCat’s tiers ($0 Free, then $110 Pro and $325 Smart on annual billing, and up) are flat per month with unlimited seats, which is genuinely attractive for a team that wants headcount off the meter. The difference is where the ceilings sit. ConfigCat gates hard on product count and flag count (its Pro tier caps at 3 products and 100 flags), so a multi-app team can be pushed up a tier by hitting a capacity limit rather than by growth in users or traffic. Flat pricing does not mean no limits; it means the limits are capacity ceilings you can see in advance, not a meter that ticks in the background. Teams that bump into those product or flag ceilings often start weighing the [flat-rate ConfigCat alternatives](/alternatives/configcat-alternative/) at that point. The catch worth naming on flat pricing is the reverse of usage-based: a very small or very low-traffic team can occasionally pay *more* on a flat plan than it would on a metered one that bills near zero at low volume. Flat pricing optimizes for predictability, not for being the rock-bottom cheapest at tiny scale. If you are below a metered tool’s free ceiling and expect to stay there, usage-based can win. The flat-pricing argument is for everyone who plans to grow and would rather not re-forecast the flag bill every quarter. ## 6. “Free” open source isn’t free: the self-host model There is a fourth option that does not fit the three meters: run an open-source flag system yourself. Unleash (AGPL-3.0), Flagsmith (BSD-3-Clause community edition), and GrowthBook (MIT core) all ship a self-hostable version with a license fee of exactly zero. For a team with a platform function and a preference for owning infrastructure, this is a legitimate and sometimes correct choice. Teams that adopted Flagsmith’s community edition and later found the operational tax too high tend to shop the [hosted Flagsmith alternatives](/alternatives/flagsmith-alternative/) once those staff hours start to bite. It is just not free, and the pricing-model framing makes the trade visible. The license is the small number. The real cost moves off the invoice and onto your team: you run the database, you run the upgrades, you run the proxy or edge layer some of these need for streaming, and you own the on-call when flag delivery breaks at 2 a.m. There are usually feature ceilings on the free edition too. Unleash’s open-source tier limits you to one project and two environments. SSO and custom roles start on its paid plan, and SSE streaming is Enterprise-only. GrowthBook self-host wants you to operate its proxy and ideally a data warehouse. The decision here is the same one covered in the [build vs buy feature flags framework](/blog/build-vs-buy-feature-flags/) : if feature management is not your product, the engineering hours you spend operating a flag platform are hours not spent on what your customers pay for. Self-hosting is buying with labor instead of dollars, and labor is rarely the cheaper currency once you price it honestly. Cost is only half of that decision, though, and [what self-hosting does to your kill switch during an incident](/blog/self-hosted-feature-flags/) is the half this section does not cover. ## 7. A real cost comparison: one team, six vendors Models are easier to judge against one concrete team. Take a mid-size SaaS app: 8 engineers shipping behind flags, around 50,000 client-side monthly active users, and a handful of backend services. Here is roughly what each vendor’s published list pricing produces for that single profile per year. The figures come straight from each vendor’s pricing page as of September 2026 and from the worked examples on our own [comparison pages](/compare/) . Approximate, list pricing, single profile. The two flat tools sit at the bottom; per-seat and usage-based stack up several times higher for the identical team. A few honesty notes, because a pricing comparison is worthless if it quietly stacks the deck. The numbers are not strictly apples-to-apples, because the models meter different things. GrowthBook’s figure is 8 seats at $40 each, and GrowthBook counts every user who logs in, read-only or not. PostHog’s figure is driven by flag *requests* rather than MAU, so it swings hard on how chatty your client-side flag checks are. Server-side local evaluation is cheaper there but still billed, because each poll for flag definitions counts as 10 requests, about 864,000 a month for one always-on server. LaunchDarkly’s figure is Foundation on annual billing with 6 service connections, the first 5 of them included, before any experimentation usage. Month to month it comes to about $6,144. Unleash’s is the cloud Pay-As-You-Go list rate, which an Enterprise contract would renegotiate. The per-vendor breakdowns, with the exact arithmetic, live on the individual comparison pages: [LaunchDarkly](/compare/featureflip-vs-launchdarkly/) , [Flagsmith](/compare/featureflip-vs-flagsmith/) , [ConfigCat](/compare/featureflip-vs-configcat/) , [Unleash](/compare/featureflip-vs-unleash/) , [GrowthBook](/compare/featureflip-vs-growthbook/) , and [PostHog](/compare/featureflip-vs-posthog/) . What survives all the caveats is the shape: flat pricing clusters at the bottom and stays there, while per-seat and usage-based climb with the two things you are trying to grow. ## 8. How to choose a model that won’t surprise you The right model is the one whose meter is attached to something you are *not* trying to maximize. Run your situation through three questions in order. **Is your team going to grow?** If you plan to add engineers, per-seat is the model most likely to outrun your budget, because the meter is the headcount you are deliberately increasing. A growing team is the strongest case for flat pricing, where adding people is free. **Is your client-side traffic going to grow, and is it unpredictable?** If yes, usage-based pricing converts your success into a variable bill you cannot easily forecast. If your usage is small, or mostly server-side on a handful of instances, the same model can be the cheapest thing available, sometimes free. Match the meter to whether your traffic is steady and small or spiky and large. **Do you want the bill to be a fixed line you can forecast a year out?** If predictability is the priority, flat pricing is the only model that delivers it, and the question becomes whether a given vendor’s capacity ceilings fit your project and flag counts. Read the tier limits, not just the price. The cross-cutting rule underneath all three: read what the meter is attached to, then ask whether that thing moves in the direction you are pushing your business. A flag tool should get cheaper to operate as you succeed, or at least hold steady. A meter bolted to your headcount or your traffic does the opposite. ## The shorter version Feature flag pricing comes in three meters and a fourth path. **Flat** pricing fixes the bill to a tier and lets your team and traffic grow underneath it for free, which is why it is the wedge newcomers compete on and the model Featureflip is built around ($0 / $49 / $99 / $249 a month, with nothing metered: no seats, MAU, or evaluations). **Per-seat** pricing ($40 to $75 a seat at GrowthBook, Unleash, and Flagsmith) looks cheap per unit but multiplies by the headcount you are trying to grow, so it scales against you. **Usage-based** pricing starts cheap and turns your own success into an unpredictable bill, and four tools meter it on four different units: LaunchDarkly per connection and per MAU, PostHog per flag request, Flagsmith per million API requests, Unleash as a traffic ceiling on top of seats. **Open-source self-hosting** is zero in license and real in operational cost. For one 8-engineer team, the flat tools landed near $948 to $1,320 a year while the per-seat and usage-based tools stacked up to $3,840 to $7,200 for the same work. Pick the model whose meter is attached to something you are not actively trying to increase. ## Frequently asked questions ### What is flat feature flag pricing? Flat feature flag pricing is a fixed monthly or annual fee for a tier, with no per-seat or per-evaluation meter on top. Within the tier’s capacity ceilings (projects, flags, environments), the bill does not change when you add engineers or get more traffic. Featureflip uses this model ($0 Free, $49 Team, $99 Pro, $249 Business per month, unlimited evaluations), as does ConfigCat. It is the model that keeps the invoice predictable as a team grows, which is why teams that expect to scale headcount or traffic tend to reach for flat pricing over a per-seat or per-usage meter. ### Why is per-seat feature flag pricing more expensive as you grow? Because the bill is the per-seat price multiplied by your team size, and team size is something you are actively trying to increase. At $40 per user (GrowthBook) or $75 per seat (Unleash), one engineer is cheap but ten engineers is ten times that, and twenty-five is twenty-five times. The unit price stays small while the multiplier climbs, so per-seat pricing scales against a growing engineering org. It can also push teams to ration flag access to control spend, which works against letting the people closest to a release operate it. ### Is usage-based feature flag pricing cheaper than flat pricing? It depends on your traffic. Usage-based pricing (per evaluation, per MAU, or per API request) bills near zero at low volume, so for a small workload it can be the cheapest option, sometimes free. The trade is predictability: the bill rises with client-side traffic, so a successful launch increases your flag cost at the same time. Flat pricing costs more at tiny scale but stays fixed as you grow. Choose usage-based if your volume is small and likely to stay small; choose flat if you expect to grow and want a forecastable bill. ### Which feature flag tools have usage-based pricing? Four of the major tools meter usage: LaunchDarkly bills per server-side service connection and per 1,000 client-side monthly active users, PostHog bills per flag request after the first million a month, Flagsmith bundles API requests into tiers and charges from $50 per additional million, and Unleash includes 53 million cloud API requests a month and charges $5 per additional million on top of its per-seat price. GrowthBook Cloud is priced per seat, with overage only past its CDN and warehouse allowances, while ConfigCat and Featureflip are flat, with capacity ceilings instead of meters. The unit differs in every case, so compare what each meter counts (connections, MAU, or raw requests) rather than treating “usage-based” as one comparable model. ### How much does a feature flag tool cost per year? For one mid-size team (8 engineers, around 50,000 client-side MAU) on list pricing as of September 2026, the range is wide because the models differ. Flat-priced tools landed lowest: roughly $948 a year for Featureflip Pro and about $1,320 for ConfigCat Pro. Per-seat tools ran higher: about $3,840 for GrowthBook, $4,800 for Flagsmith, and $7,200 for Unleash cloud. Usage-based tools depend on traffic but reached roughly $5,520 for PostHog and $5,118 for LaunchDarkly Foundation on annual billing at that profile. The same team, the same job, an order-of-magnitude spread driven entirely by the pricing model. ### Is open-source feature flagging actually free? The license is free; the system is not. Unleash, Flagsmith, and GrowthBook all offer self-hostable open-source editions with no license fee, but you take on hosting, database operation, upgrades, any proxy or edge layer they need, and the on-call when flag delivery fails. There are usually feature ceilings on the free edition too (Unleash limits open source to one project and two environments, with SSO on its paid plan and streaming on Enterprise). Self-hosting trades a subscription for engineering labor, which is the right call when you have a platform team and feature management is core to your product, and the wrong call when those hours would be better spent on what your customers pay for. If you are mid-evaluation, the [comparison pages](/compare/) break down each vendor’s pricing and features side by side, the [shortlist of feature flag tools](/feature-flag-tools/) groups the whole category by the meter each vendor runs, and the [build vs buy decision framework](/blog/build-vs-buy-feature-flags/) covers the case where the real alternative is a homegrown system rather than another vendor. Featureflip is the focused, flat-priced answer this guide points toward: $0 / $49 / $99 / $249 a month with no per-seat fees, no per-MAU charges, and unlimited evaluations on every plan, so the bill stays still until your team or traffic outgrows a tier. The [pricing page](/pricing/) has the full numbers, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-as-code-terraform/ # Feature Flags as Code: Managing Featureflip with Terraform [Blog](/blog/) # Feature Flags as Code: Managing Featureflip with Terraform Manage feature flags as code with the Featureflip Terraform provider: declare projects, flags, and targeting in .tf files, and know what to leave out. July 30, 2026 · Updated September 14, 2026 · - terraform - automation - tutorials Flag configuration is the last piece of production most teams still change by clicking. Your infrastructure is declared, reviewed, and reproducible. The targeting rule deciding which 10% of users hit the new checkout was typed into a dashboard on a Tuesday by someone who has since left. Managing feature flags as code closes that gap, and the [Featureflip Terraform provider](/docs/integrations/terraform/) is how you do it. Projects, environments, flags and their variations, segments, SDK keys, and per-environment targeting are all declarable in `.tf` files and applied through the same pipeline as the rest of your infrastructure. This guide covers the install, a working configuration, importing flags you already have, and the more interesting question underneath: which parts of a flag belong in code, and which parts should never go there. **Key Takeaways** - The provider manages six resource types through the public Management API, so flag structure gets the same review and history as your VPC. - Declaring a Boolean flag is four lines. Variations are auto-seeded, so you only spell them out for String, Number, and Json flags. - **rules is authoritative.** Once Terraform manages a flag-environment’s rules, it removes any rule added in the dashboard on the next apply. - Keep day-to-day toggling out of Terraform. A kill switch during an incident should not wait on a plan and apply. - Existing flags import cleanly with `/`, so adoption is incremental rather than a rebuild. ## 1. Why flag config drifts out of review A feature flag platform is configuration that changes behavior in production, which makes it infrastructure by every definition except the one your review process uses. The result is a familiar asymmetry. Changing a load balancer takes a pull request and two approvals. Changing which users see an unreleased checkout flow takes a dropdown and a save button. That asymmetry is fine for the thing flags are *for*. Flipping a flag off during an incident has to be fast, and routing that through CI would defeat the point. It stops being fine for the surrounding structure: which flags exist, what variations they carry, which segments they target, and what the rollout rules actually say. That structure is long-lived and it accretes. Nobody can diff a dashboard. This guide covers the tool that closes the gap. The process that grows around it, meaning what a reviewer checks in a flag diff and how CI applies the result, is [the GitOps workflow for feature flags](/blog/gitops-feature-flags/) . ## 2. Install and authenticate The provider is published as `canopy-labs/featureflip` on the Terraform Registry and the OpenTofu Registry, so the block below works in either tool as written. ```hcl terraform { required_providers { featureflip = { source = "canopy-labs/featureflip" version = "~> 0.1" } } } provider "featureflip" { organization = "acme-corp" # token via FEATUREFLIP_TOKEN (recommended), or: token = var.featureflip_token } ``` 0.1" } }}provider "featureflip" { organization = "acme-corp" # token via FEATUREFLIP_TOKEN (recommended), or: token = var.featureflip_token}"> Authentication uses the same bearer token as the [Management API](/blog/feature-flag-management-api/) . Create one in the dashboard under Organization Settings, since tokens cannot be minted through the public API: - **Service token (ffs_…)**, recommended for CI. Scoped to one organization with a role and an optional project allowlist. Use Member or Admin, because Viewer tokens cannot write. - **Personal access token (ffp_…)**, which acts as you across your organizations. Better suited to local experimentation than a pipeline. Set it through `FEATUREFLIP_TOKEN` rather than putting it in configuration. ## 3. Declare a project and your first flags A Boolean flag is four required attributes. Variations are seeded automatically, so there’s nothing to spell out: ```hcl resource "featureflip_project" "web" { key = "web" name = "Web App" description = "Customer-facing web application" } resource "featureflip_feature_flag" "new_checkout" { project = featureflip_project.web.key key = "new-checkout" name = "New Checkout Flow" type = "Boolean" tags = ["checkout"] } ``` Non-Boolean flags declare their variations inline. The `type` attribute is immutable, so a flag cannot be converted from Boolean to String later without being replaced: ```hcl resource "featureflip_feature_flag" "banner_text" { project = featureflip_project.web.key key = "banner-text" name = "Banner Text" type = "String" variations = [ { key = "control", name = "Control", value = "Welcome!" }, { key = "variant", name = "Variant", value = "Hi there!" }, ] } ``` For Json flags, `value` holds the JSON document as a string. Variation keys are immutable too, which matters because your [SDK](/docs/sdks/overview/) calls reference them. ## 4. Targeting rules and per-environment state `featureflip_flag_environment` is where a flag becomes real. It carries the on/off state for one environment, the fallthrough variation, prerequisites, and the targeting rules. List position sets rule priority: ```hcl resource "featureflip_segment" "beta_users" { project = featureflip_project.web.key key = "beta-users" name = "Beta Users" conditions = [ { attribute = "email", operator = "EndsWith", values = ["@acme.io"] }, ] } resource "featureflip_flag_environment" "new_checkout_prod" { project = featureflip_project.web.key flag = featureflip_feature_flag.new_checkout.key environment = "production" enabled = true default_variation = "false" rules = [ { description = "Beta users always get the new checkout" variation = "true" segment = featureflip_segment.beta_users.key }, { description = "50% rollout in Germany" variation = "true" rollout_percentage = 50 condition_groups = [ { operator = "And" conditions = [ { attribute = "country", operator = "Equals", values = ["DE"] }, ] }, ] }, ] } ``` Reviewing that diff tells you something a dashboard screenshot never will: the rollout went from 10 to 50 percent, someone approved it, and the commit says why. ## 5. Draw the line at toggling This is the part worth getting right before you adopt the provider widely. The split that holds up in practice. Structure is reviewed, toggling stays fast. Turning a flag off during an incident is a dashboard or API action. Sending it through a plan and apply adds minutes to your recovery time for no benefit, and the [kill switch](/use-cases/kill-switch/) exists precisely because those minutes matter. The pattern most teams settle on is to manage projects, environments, flags, and segments in Terraform, then leave day-to-day toggling alone. Targeting sits in between, and it comes with a rule you need to know before you adopt it. ## 6. rules is authoritative Once you set `rules` on a `featureflip_flag_environment`, Terraform owns every rule in that flag-environment. A rule someone adds through the dashboard is removed on the next apply, without a prompt beyond the plan output. That’s the correct behavior for declarative infrastructure, and it’s still a sharp edge if half your team hasn’t been told. Two ways to live with it: put targeting under Terraform for the flags whose rules are stable and reviewed, and leave `rules` unset on the flags your team iterates on daily. Leaving it unset means Terraform manages the flag’s existence and its on/off state while ignoring the rules entirely. Destroying a `featureflip_flag_environment` deletes its managed rules and disables the flag in that environment. The fallthrough configuration stays in place. ## 7. Import the flags you already have Nobody adopts this on a greenfield project. Both resources import with a path-style ID: ```bash terraform import featureflip_feature_flag.new_checkout web/new-checkout terraform import featureflip_flag_environment.new_checkout_prod web/new-checkout/production ``` The flag-environment import brings in identifiers only. Rules and prerequisites are not pulled into state, so add them to your configuration explicitly if you want Terraform to manage them. That’s a deliberate choice: it means importing a flag can’t silently hand Terraform ownership of targeting you hadn’t reviewed. Import one flag, run a plan, confirm it’s empty, then widen. A plan that comes back empty is the signal that your configuration matches reality. ## 8. Two things that bite **SDK keys are sensitive, and destroying one revokes it.** The plaintext value of a `featureflip_sdk_key` is available only at creation and is stored in Terraform state afterwards, so treat that state as a secret. Destroying the resource revokes the key, which cannot be undone, and any service still holding it stops evaluating. ```hcl resource "featureflip_sdk_key" "backend" { project = featureflip_project.web.key environment = "production" name = "backend" type = "ServerSide" } output "sdk_key" { value = featureflip_sdk_key.backend.key sensitive = true } ``` **Flag type and variation keys are immutable.** Changing either forces replacement, and replacing a flag your code already reads is a production change rather than a config tweak. Decide the type when you create the flag. ## The shorter version The Featureflip Terraform provider manages projects, environments, flags and variations, segments, SDK keys, and per-environment targeting through the public Management API. Install it from the Terraform or OpenTofu Registry as `canopy-labs/featureflip`, authenticate with a service token through `FEATUREFLIP_TOKEN`, and declare a Boolean flag in four attributes. Managing feature flags as code works best on the structural pieces, where review and history are worth having. Leave incident-time toggling to the dashboard, because a plan and apply is the slower path to a kill switch. Remember that `rules` is authoritative once set, and import existing flags with `/` so adoption is incremental. ## Frequently asked questions ### Is there a Terraform provider for feature flags? Yes. Featureflip publishes one on the Terraform Registry as [`canopy-labs/featureflip`](https://registry.terraform.io/providers/canopy-labs/featureflip/latest) . It manages projects, environments, feature flags and their variations, user segments, SDK keys, and per-environment state and targeting rules, all through the public Management API. Install it with a standard `required_providers` block and authenticate with a service token. ### Does the Featureflip Terraform provider work with OpenTofu? Yes. The provider is listed on the [OpenTofu Registry](https://search.opentofu.org/provider/canopy-labs/featureflip/latest) under the same `canopy-labs/featureflip` address, and `tofu init` verifies the downloaded release against the Canopy Labs signing key. Your HCL carries over untouched. On the command line, `tofu plan`, `tofu apply`, and `tofu import` take the same arguments as their `terraform` counterparts. ### Should feature flags be managed as code? The structural parts, yes. Managing feature flags as code pays off for the configuration that outlives any one release. Which flags exist, what variations they carry, which segments they target, and what the targeting rules say are all long-lived configuration that benefits from review, history, and reproducible environments. The exception is toggling. Turning a flag off during an incident needs to be immediate, and routing it through a plan and apply adds minutes to recovery for no gain. Manage structure in Terraform and leave the fast actions to the dashboard or API. ### What happens if someone edits a Terraform-managed flag in the dashboard? It depends on the attribute. Once `rules` is set on a `featureflip_flag_environment`, Terraform owns every rule there and removes dashboard-created rules on the next apply. If you leave `rules` unset, Terraform manages the flag’s existence and state while ignoring its rules, which is the right choice for flags your team iterates on daily. ### Can I import feature flags I already created? Yes, and this is the normal adoption path. `terraform import featureflip_feature_flag.example /` brings an existing flag under management, and flag-environments import with `//`. The flag-environment import brings in identifiers only, so rules and prerequisites stay unmanaged until you add them to your configuration. ### Does the Terraform provider evaluate flags? No. It manages configuration, the same surface the dashboard and the [Management API](/blog/feature-flag-management-api/) act on. Reading a flag’s value for a user at runtime is the job of the [SDKs](/docs/sdks/overview/) , which evaluate locally in memory on your request path. Configuration and evaluation stay on separate surfaces, which is what keeps flag reads fast. Featureflip is a flat-priced feature flag platform, and flags-as-code is included rather than reserved for an enterprise tier: the [Terraform provider](/docs/integrations/terraform/) and the [public Management API](/blog/feature-flag-management-api/) work on every plan, alongside the [MCP server](/blog/feature-flags-mcp-server/) if you’d rather drive flags from your editor. See the [provider docs](/docs/integrations/terraform/) for the full resource reference, or [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-for-ai-apps/ # Feature Flags for AI Apps: Roll Out a New LLM Model Safely [Blog](/blog/) # Feature Flags for AI Apps: Roll Out a New LLM Model Safely Feature flags for AI apps: put the model, prompt, and settings behind one flag, roll a new LLM out with sticky bucketing, and keep a fail-safe kill switch. July 10, 2026 · - ai - tutorials The riskiest change in an AI feature is usually the one that looks smallest: swap the model, or edit the system prompt. A new model can be cheaper and sharper on your evals and still regress on the one prompt shape your users actually send. The usual way to make that change, an environment variable and a redeploy, gives you exactly one setting for the whole fleet and one way to undo it: ship again. There is no way to send the new model to 10 percent of users, keep each of them on it consistently, and pull it back in seconds when it misbehaves. A feature flag holding the model configuration gives you all three. This is the hands-on companion to [using feature flags in an AI or LLM app](/use-cases/feature-flags-for-ai/) : where that page covers the shape of the idea, this one is the walkthrough. Put the model, prompt, and settings in one JSON flag, read it on the request path, ramp a new model from 10 to 100 percent with per-user sticky bucketing, and make the fallback config a kill switch. The examples use the [Python SDK](/docs/sdks/python/) ; the same flag works from any language Featureflip supports. **Key Takeaways** - Hold the model name, system prompt, temperature, and token limit in **one JSON feature flag**, so changing the model or tuning the prompt is a dashboard edit, not a deploy. - Read it on the request path with `client.variation("llm-model-config", ctx, default=SAFE_CONFIG)`. The default is a known-good config the SDK serves during an outage, so the safe model doubles as the fail-safe fallback. - Migrate a new model with a **percentage rollout**: 10, then 50, then 100. Deterministic sticky bucketing keeps each user on one model as you ramp, so a single conversation never switches models between turns. - The flag is a **kill switch**: flip it or ramp back to zero and every SDK reverts within a second or two over streaming, fleet-wide, with no redeploy. Reads come from cached config, so the switch survives a control-plane blip. - The flag owns the config and the routing decision. Whether the new model is *better*, and what it *costs*, stays with your eval and observability stack. ## 1. Why a model swap needs a flag, not an env var Model choice is configuration that changes often and can go wrong quietly, and an environment variable is the wrong container for it in three specific ways. It is all-or-nothing. A `MODEL=gpt-4o` variable is one value for every process. You cannot give it to a tenth of your users while you watch the numbers, because there is no per-user dimension to an env var. The only rollout it supports is “everyone, on the next deploy.” It is not sticky. Push a config change or restart a pod and every in-flight user flips at once. If a customer is three turns into a conversation, their next reply comes from a different model with a different temperature, and the thread reads like two assistants stitched together. It is not fail-safe. When the new model starts timing out or returning garbage, the old value is gone from the running process. Recovering means editing config and shipping again, on the worst possible day to be running a deploy. Holding the model config in a flag fixes all three. A [percentage rollout](/glossary/percentage-rollout/) gives you the per-user ramp. Deterministic [sticky bucketing](/glossary/sticky-bucketing/) keeps each user on one model across the ramp. And the default you pass at evaluation is a fallback the SDK serves when it cannot reach the control plane, so a bad model is one dashboard flip away from gone rather than one deploy away. In short, holding a model name and prompt in a flag is a form of [remote config](/glossary/remote-config/) , with a rollout and a kill switch attached. ## 2. Put the model config in one JSON flag Create a JSON flag, `llm-model-config`, whose value carries the whole configuration for the feature: the model name, the system prompt, and the generation settings. One flag holds it all, so the pieces change together. In your code, build the client once and read that flag with a known-good default. In the Python SDK a single `variation` call covers every type: pass a dict default and you get a dict back, so a JSON flag needs no special method. ```python import os from featureflip import FeatureflipClient client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) # The default is a model config you trust. The SDK serves it if it # cannot reach Featureflip, so an outage falls back to a known-good # model rather than an experimental one. SAFE_CONFIG = { "model": "gpt-4o-mini", "system_prompt": "You are a helpful support assistant.", "temperature": 0.3, "max_tokens": 1024, } def model_config_for(user): ctx = {"user_id": user.id, "tenant_id": user.tenant_id, "plan": user.plan} return client.variation("llm-model-config", ctx, default=SAFE_CONFIG) ``` The `default` argument is doing double duty. It is the value returned if the flag is missing or the SDK is briefly cut off from Featureflip, and it is your statement of what “safe” means for this feature. Make it a model and prompt you would be comfortable serving to everyone, because during an outage that is exactly what happens. The [Python quickstart](/docs/quickstart/python/) has the minimal client setup; the rest of this guide is what you build on top of it. ## 3. Read the config on the request path Reading the flag happens right before you build the model request. Evaluation is an in-memory lookup against a ruleset the SDK already holds, so it adds no network hop ahead of the model call, where latency is felt most. ```python def generate_reply(user, user_message): config = model_config_for(user) return llm.chat( model=config["model"], temperature=config["temperature"], max_tokens=config["max_tokens"], messages=[ {"role": "system", "content": config["system_prompt"]}, {"role": "user", "content": user_message}, ], ) ``` The application code never names a model. It asks the flag which configuration applies to this user and builds the request from the answer. That indirection is the whole point: the model, the prompt, and the settings are now values you change from a dashboard, and the code that calls the provider stays fixed. ## 4. Roll a new model out 10, then 50, then 100 To migrate from the current model to a new one, give the flag two variations, a current-model config and a new-model config, and put a percentage rollout on it. Start the new variation at 10 percent, watch your evals and cost, then raise it to 50 and 100 as the signal holds. Rolling back is the same move in reverse and takes effect in seconds. The property that makes this safe for a conversational feature is stickiness. Bucketing is deterministic: the SDK hashes the context key you bucket on, so the same `user_id` lands in the same bucket every time, in every worker and every replica. A user who is in the new-model group stays there as you raise the percentage, so a single conversation does not switch models between turns, and you never need sticky sessions or a shared cache to hold the assignment. Each stage widens the new-model share without reassigning the users already on it. You change the percentage in the dashboard; no deploy, and every SDK picks up the new split over its stream. Because a JSON flag serves a whole config object, this is a [multivariate](/glossary/multivariate-testing/) rollout, not just on versus off: each variation is a full model configuration, and the rollout decides which one a given user gets. You raise and lower the percentage from the dashboard, and every connected SDK reflects the new split on its next evaluation. The mechanics are the same [rollout strategies](/docs/concepts/rollout-strategies/) a UI feature uses, pointed at model choice. ## 5. Make the fallback a kill switch A gradual rollout limits the blast radius of a bad model. A kill switch removes it. Both come out of the same flag. The manual switch is the flag itself. When a model starts hallucinating, slowing down, or running up cost, ramp its rollout back to zero or flip the flag to the safe variation. Every SDK picks up the change over a Server-Sent Events stream within a second or two, fleet-wide, with no redeploy. Because reads are served from a local cache of the config, the switch keeps working even if the control plane is briefly unreachable, and the default you passed is the ultimate backstop. The automatic switch is in your code. Wrap the model call so a failure retries with the safe config rather than surfacing an error to the user: ```python def generate_reply(user, user_message): config = model_config_for(user) try: return call_model(config, user_message) except (ModelError, TimeoutError): # The chosen model failed. Fall back to the known-good config. # Flip 'llm-model-config' in the dashboard to move the whole # fleet off the bad model in seconds, with no redeploy. return call_model(SAFE_CONFIG, user_message) ``` The one rule that keeps this honest: the default and the fallback are the model you trust, never the experimental one. If the new model is the fallback, a control-plane blip or a caught error routes traffic *to* the unproven model, which is backwards. Default to known-good, and let a rollout or a rule opt users into the new one. The same pattern powers a general [feature flag kill switch](/use-cases/kill-switch/) for any code path, not just model calls. ## 6. Route a model or prompt per tenant Some customers need a specific model regardless of where the migration stands: an enterprise account pinned to a particular model, a cohort on a different prompt, or a tenant held on the old model while everyone else moves. A [targeting rule](/docs/guides/targeting-rules/) handles it. Add a rule on an attribute you already pass in the context, such as `tenant_id` or `plan`, and serve that segment a specific variation. Rules run top to bottom and the first match wins, so an override for a named tenant sits above the broad rollout and takes precedence over it. Pinning a customer to a model, or releasing them back into the migration, is a dashboard change that every SDK reflects on its next evaluation. The attributes and operators available are covered in the [targeting docs](/docs/concepts/targeting/) . ## 7. What the flag does not do A flag delivers and routes the model configuration inside your application. It is deliberately not an AI platform, and keeping the boundary clear keeps you from asking it to do a job it cannot. It does not tell you the new model is better. Rolling a model to 100 percent because it did not throw is not the same as knowing it improved your responses. Scoring output quality and catching regressions is the work of an eval or observability tool. The flag ships the model; those tools grade it. If your goal is a statistically valid comparison rather than a safe migration, that is an experiment, and the delivery half looks like [A/B testing with feature flags](/use-cases/a-b-testing/) with your analytics calling the winner. It does not track token cost or latency. Moving traffic between models changes both, but the numbers live in your metrics and tracing stack. The flag can shift the traffic; observability tells you what the shift cost. It is not a prompt store or a secrets manager. The flag holds the prompt that is live right now and switches it. Authoring, versioning, and diffing every prompt you have written belongs in your repo or a prompt tool. And provider API keys stay in your secrets manager, never in the flag value, especially since client SDKs evaluate config in the browser. Those boundaries are the same honest split the [AI use-case page](/use-cases/feature-flags-for-ai/) draws: the flag owns config and routing; your eval, observability, prompt, and secrets tools own the rest. Many AI features need the flag from day one and add the other tooling later, when the migration should be driven by data rather than by whether the model errored. ## The shorter version Swapping an LLM or editing a system prompt is a config change that an environment variable handles badly: all-or-nothing, not sticky, not fail-safe. Put the model name, system prompt, temperature, and token limit in one JSON feature flag instead, and read it on the request path with `client.variation("llm-model-config", ctx, default=SAFE_CONFIG)`, where the default is a known-good config the SDK serves during an outage. Migrate a new model with a percentage rollout, 10 then 50 then 100, and deterministic sticky bucketing keeps each user on one model so a conversation never switches mid-thread. The flag doubles as a kill switch: ramp to zero or flip to the safe variation and every SDK reverts over streaming in seconds, no deploy, and the switch survives a control-plane blip because reads come from cached config. Add a targeting rule to pin a tenant to a specific model. What the flag does not do, judge whether the model is better or what it costs, stays with your eval and observability stack. ## Frequently asked questions ### How do I roll out a new LLM model gradually? Put the model configuration in a JSON feature flag with two variations, the current model and the new one, and add a percentage rollout. Start the new variation at 10 percent, raise it to 50 and 100 as your evals and cost hold, and roll back in seconds by lowering the percentage. Deterministic sticky bucketing means the same user stays in the same group as you ramp, so the users already on the new model are not reshuffled when you widen its share. You change the percentage in the dashboard, and every SDK picks up the new split over its stream with no redeploy. ### How do I keep a user on the same model during a conversation? Bucket the rollout on a stable key such as the user id or a conversation id. Featureflip hashes that key deterministically, so it maps to the same bucket in every worker and every replica, and a user assigned to the new model at 10 percent is still on it at 50 and 100. That stickiness is what stops a single conversation from switching models between turns, and it holds without sticky sessions or a shared assignment cache because every SDK computes the same bucket from the same key. ### Can I use a feature flag as an LLM kill switch? Yes. Hold the model config behind a flag and give the evaluation a known-good fallback. When a model starts erroring, hallucinating, or running slow or expensive, flip the flag to the safe variation or ramp its rollout to zero, and every connected SDK reverts within a second or two over streaming, fleet-wide, with no redeploy. Because reads are served from a local cache of the config, the switch keeps working even if the control plane is briefly unreachable, and the default you pass at evaluation is the ultimate backstop. ### Should I store the system prompt and temperature in a feature flag? Store the live selection there. One JSON flag can hold the model name, system prompt, temperature, and max_tokens as a single value, so the whole configuration changes together and ships to every instance at once. Keep the authoring and version history of your prompts in your repo or a dedicated prompt tool, and keep provider API keys in your secrets manager rather than the flag. The flag is where you choose which configuration is live, not where you archive every version or store secrets. ### Do feature flags add latency to each model call? No. With a server SDK, flag evaluation is an in-memory read against a ruleset the SDK already holds, so reading the model config is a local lookup with no network round trip. That read runs in the time of a dictionary lookup and finishes long before the model call, which is where the real latency is. The SDK keeps the ruleset current over a streaming connection in the background, so a config change reaches your process without adding any per-request work. Featureflip is a focused, flat-priced feature flag platform, and it puts your AI feature’s model, prompt, and settings behind one flag: swap models without a deploy, migrate gradually with sticky per-user bucketing, and kill a bad model in seconds with a fail-safe fallback. Because it uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations, a high-throughput AI service never sees its flag bill scale with request volume. And when an AI assistant is the one writing the code, it can manage the flags too: the [MCP server lets Claude Code and Cursor](/blog/feature-flags-mcp-server/) create, ramp, and retire flags from the editor. Read the [SDK overview](/docs/sdks/overview/) to pick your language, see the [pricing page](/pricing/) for the numbers, and [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-in-go/ # Go Feature Flags: In-Process Evaluation, No cgo or Sidecar [Blog](/blog/) # Go Feature Flags: In-Process Evaluation, No cgo or Sidecar Go feature flags with a pure-Go SDK: no cgo, no dependencies, in-process evaluation with no per-request network call, goroutine-safe reads, clean shutdown. June 26, 2026 · - go - golang - tutorials Most feature flag tutorials stop at “read a flag, branch on it.” In Go that is the easy 10%. The other 90% is everything the language makes you decide on purpose: whether the SDK drags cgo into a binary you wanted to ship to `scratch`, what happens to the first requests before flags have loaded, whether a value read concurrently from a thousand goroutines needs a lock you have to hold, and how the background work shuts down without leaking a goroutine. A Go service is long-lived, heavily concurrent, and usually deployed as a single static binary. Those three properties are exactly what a feature flag library has to respect. This guide adds feature flags to a Go HTTP service with the [Go server SDK](/docs/sdks/go/) , which evaluates flags locally in memory so a check is a synchronous read with no network round trip. It is a pure-Go package: standard-library `net/http` only, no external dependencies and no cgo, so it cross-compiles and ships in a `scratch` image like the rest of your binary. For a copy-paste first flag, the [Go quickstart](/docs/quickstart/go/) is the five-minute version. This post is about the operational decisions a real Go service makes around it. **Key Takeaways** - The Go SDK is pure Go with no cgo and no external dependencies, so `CGO_ENABLED=0 go build` produces a static binary that runs in a `scratch` image with nothing else copied in. - `featureflip.Get()` blocks until the first flag fetch finishes and returns an `error`, so a successful return means flags are already in memory. There is no “boot window serving defaults” to engineer around. - Variation methods read from an in-memory store behind a `sync.RWMutex`, so `client.BoolVariation(...)` is safe to call concurrently from any handler goroutine with no external locking. - One client per process. Create it once in `main`, pass it down, and a single background goroutine keeps the ruleset current over SSE for the whole service. - `defer client.Close()` stops that goroutine and flushes buffered analytics. Skip it and the goroutine plus its streaming connection leak until the process dies. ## 1. What makes feature flags in Go different A flag lives in one of three very different homes, and the Go case sits at the demanding end. In a one-off script, flags barely register: load config, read a value, do the work, exit. In a browser tab the constraint is the render, which is why a [React app](/blog/feature-flags-in-react/) cares about avoiding extra re-renders and a [Next.js app](/blog/feature-flags-in-nextjs/) cares about the hydration flash. A Go service is the third home, and it has its own shape: it boots once, stays up for days, fields highly concurrent traffic across many goroutines, and ships as a self-contained binary you would rather run in a minimal container with no interpreter and no shared libraries. That shape rewards local evaluation. The SDK fetches your full ruleset once at startup, keeps it current over a streaming connection, and answers every variation call from memory. The long-lived process pays the setup cost once and then gets effectively free flag checks for the rest of its life, and the heavy concurrency is fine because reads are lock-protected and never touch the network. The same local-evaluation model drives the [Node.js server guide](/blog/feature-flags-in-nodejs/) ; the difference here is that Go’s static-binary deployment and explicit concurrency change how you wire it up. The rest of this guide walks those decisions in order. ## 2. Install a pure-Go SDK that ships in a scratch image Install the package: ```bash go get github.com/canopy-labs/featureflip-go/v2 ``` The SDK requires Go 1.25+. What it does not require is anything else: it talks to the evaluation API through the standard library’s `net/http`, pulls in no third-party modules, and uses no cgo. That last point is the one that bites people in Go. A flag library that links a C dependency forces `CGO_ENABLED=1`, which means a C toolchain in your build image and a binary that is no longer fully static, so it cannot run in `scratch` and cross-compilation stops being a one-liner. Because this SDK is pure Go, none of that applies. A standard static build drops straight into an empty base image: ```dockerfile # build stage FROM golang:1.25 AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app ./cmd/server # runtime: the binary and nothing else FROM scratch COPY --from=build /app /app ENTRYPOINT ["/app"] ``` The flag SDK adds no system packages to that final stage and does not change your `CGO_ENABLED=0` setting, so adding feature flags does not cost you the static-binary deployment you build Go services to get. Cross-compiling for another architecture with `GOOS`/`GOARCH` keeps working for the same reason. ## 3. Initialize once, and let Get block until flags are loaded Create the client a single time in `main`, check the error, and defer the cleanup: ```go package main import ( "log" "os" featureflip "github.com/canopy-labs/featureflip-go/v2" ) func main() { client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY")) if err != nil { log.Fatalf("feature flags failed to load: %v", err) } defer client.Close() // build your server with `client` and start serving } ``` `featureflip.Get()` is a blocking call. It fetches the initial ruleset over HTTPS and only returns once that fetch succeeds, or returns an `error` if it fails or exceeds the init timeout (10 seconds by default, adjustable with `featureflip.WithInitTimeout`). This is the part that makes the Go init story simpler than the equivalent in a runtime where the client is created asynchronously. By the time `Get` returns a non-nil client and a nil error, the flags are in memory, so the first request your server handles already evaluates against real configuration. There is no window where the process is up but flag-blind, and therefore nothing to gate a readiness probe on for correctness: a `log.Fatalf` on the error from `Get` turns a failed flag fetch into a failed boot, which is exactly what you want from a hard dependency. (`client.Initialized()` exists and returns `true` after a successful `Get`, so it is mostly useful as a defensive assertion rather than a probe.) Pass that one `client` down to your handlers rather than reaching for a fresh one anywhere. The client owns a long-lived streaming connection and a background goroutine, so one per process is the whole contract. Holding the SDK key in the `FEATUREFLIP_SDK_KEY` environment variable also lets you call `featureflip.Get("")` and let the SDK read it, which keeps the key out of your source. ## 4. Evaluate flags from any goroutine, with no network hop Once `Get` has returned, evaluation is a synchronous in-memory read. A variation call takes no `context.Context` for I/O, makes no request, and cannot time out: ```go func dashboardHandler(client *featureflip.Client) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := featureflip.EvaluationContext{ UserID: userID(r), Attributes: map[string]any{"plan": userPlan(r)}, } if client.BoolVariation("new-dashboard-nav", ctx, false) { renderDashboardV2(w, r) return } renderDashboardV1(w, r) } } ``` The third argument is the default, returned if the flag is missing or evaluation fails, so a flag check has no error path of its own. The same pattern covers the other value types: `client.StringVariation("banner-color", ctx, "blue")`, `client.Float64Variation("rate-limit", ctx, 100.0)`, and `client.JSONVariation("ui-config", ctx, map[string]any{})`. (Go decodes every JSON number as `float64`, so numeric flags use `Float64Variation`.) When you need to know *why* a value came back, `client.VariationDetail(...)` returns an `EvaluationDetail` carrying the `Reason`, the matched `Variation`, and the `RuleID`. The property that matters for a Go service is concurrency. Every Go HTTP handler runs in its own goroutine, so a busy endpoint evaluates flags from many goroutines at once. The SDK keeps the ruleset in a store guarded by a `sync.RWMutex`, which means the variation methods are safe to call concurrently without any locking on your side. You can scatter `BoolVariation` calls through middleware, handlers, and deep service functions on every goroutine in the pool, and each one is a read-locked map lookup rather than a request waterfall. Every per-request flag check stays inside the process: handlers and goroutines read from an in-memory ruleset under a read-write lock. The only network connection is the background SSE stream that keeps that ruleset current, so request handling never makes a flag call over the wire. ## 5. Target users and keep rollouts consistent across replicas The `EvaluationContext` you pass on each call carries the data your rules run against: `UserID` for percentage rollouts, and an `Attributes` map for everything else. ```go ctx := featureflip.EvaluationContext{ UserID: "u-8a31", Attributes: map[string]any{ "plan": "pro", "country": "US", }, } if client.BoolVariation("priority-queue", ctx, false) { // route this request through the new worker pool } ``` A [targeting rule](/docs/concepts/targeting/) on `plan` or `country` and a [percentage rollout](/docs/concepts/rollout-strategies/) anchored on `UserID` both resolve locally against that context, because the SDK runs the same evaluation logic in-process that the platform would run anywhere. Production Go services rarely run as one instance. They run as several replicas behind a load balancer, and a request for a given user can land on any of them. Local evaluation handles this without a shared cache to invalidate. Each instance holds its own in-memory copy of the ruleset and its own streaming connection, and bucketing is deterministic: the same `UserID` hashes to the same position in a [percentage rollout](/glossary/percentage-rollout/) on every replica, so a 10% rollout is the same 10% of users no matter which pod answers. You do not need sticky sessions to keep a user on one side of a rollout. When you flip a flag in the dashboard, the change pushes over each instance’s stream within milliseconds, so a [kill switch](/glossary/kill-switch/) thrown during an incident reaches the whole fleet at once with nothing to redeploy. If a stream drops, the SDK keeps serving the last known config while it reconnects, and if five attempts in a row fail it also polls every 3 seconds until the stream is back, so a network blip degrades to slightly staler flags rather than to defaults. ## 6. Shut down cleanly and don’t leak the goroutine The client runs a background goroutine for streaming and batches analytics events, the exposure data behind a [percentage rollout](/docs/concepts/rollout-strategies/) plus any `client.Track(...)` calls you make. Two things have to happen on shutdown: the events still in the current batch need to flush, and that goroutine plus its streaming connection need to stop. Both are `client.Close()`, which is why the `defer client.Close()` next to `Get` matters. A process that exits without it drops the pending event batch, and a process that recreates clients without closing them leaks a goroutine and an open connection each time. Wire `Close()` into a graceful shutdown alongside the HTTP server: ```go func main() { client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY")) if err != nil { log.Fatalf("feature flags failed to load: %v", err) } defer client.Close() // flushes events, stops the streaming goroutine srv := &http.Server{Addr: ":8080", Handler: routes(client)} go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }() ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("graceful shutdown failed: %v", err) } } ``` On `SIGTERM`, `signal.NotifyContext` cancels the context, the server drains in-flight requests, and the deferred `client.Close()` then flushes the last event batch and stops the goroutine before `main` returns. `Close()` is safe to call more than once and returns `nil` today, so a defensive double-close in a more complex shutdown path does no harm. ## 7. Test flag-gated code with ForTesting A flag introduces two branches, and both need tests that do not reach the network or stand up a server. `featureflip.ForTesting` is a package-level constructor that returns a fully initialized client backed by hardcoded values, with no streaming goroutine and no connections: ```go func TestCheckout_newFlow(t *testing.T) { client := featureflip.ForTesting(map[string]any{ "new-checkout-flow": true, "rate-limit": 500.0, }) defer client.Close() ctx := featureflip.EvaluationContext{UserID: "test-user"} if got := client.BoolVariation("new-checkout-flow", ctx, false); !got { t.Fatal("expected new checkout flow to be enabled") } } ``` Inject the test client wherever your code expects a `*featureflip.Client` and assert each branch independently. The override client returns its configured values regardless of the evaluation context and is ready the moment it is created, so there is no init to await and no HTTP to mock. Flip the override map to cover the off-path in a second test. ## 8. Why a first-party SDK, and what it meters You could skip the SDK and call a flag endpoint over HTTP, or poll it and cache the result yourself. That holds up until the endpoint is slow or down, at which point every request that checks a flag is either waiting on a network call or handling its failure, and you now maintain the cache, the refresh loop, the SSE reconnection, and the goroutine lifecycle by hand. Local evaluation is that machinery, built once and kept in sync with the dashboard for you, the kind of undifferentiated plumbing a [build-versus-buy decision](/blog/build-vs-buy-feature-flags/) usually settles in favor of buying. Pricing lands on a Go service in a specific way. A backend evaluates flags on nearly every request, so on a platform that bills by evaluation or by monthly active user, the flag bill tracks traffic, and a launch that triples requests triples the meter. Featureflip uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations on every plan, so request volume never moves the invoice. The [pricing page](/pricing/) has the numbers, and the [comparison pages](/compare/) show how that lands against per-MAU and usage-billed vendors for a high-throughput service. ## The shorter version What makes feature flags in Go different from a script or a browser is the deployment shape: a long-lived, concurrent, single-binary service. Install `github.com/canopy-labs/featureflip-go/v2`, a pure-Go SDK with no cgo and no dependencies, so `CGO_ENABLED=0 go build` still ships to `scratch`. Create one client in `main` with `featureflip.Get(sdkKey)`, which blocks until flags are loaded and returns an `error`, so a successful return means the first request already evaluates against real config and a failed fetch fails the boot. Evaluate with `client.BoolVariation("key", ctx, default)`, a synchronous in-memory read guarded by a `sync.RWMutex` that is safe to call from every handler goroutine. Across replicas, each instance streams config independently and buckets the same `UserID` the same way, so a dashboard flip reaches the fleet in milliseconds with no redeploy. Defer `client.Close()` to flush analytics and stop the background goroutine, and use `featureflip.ForTesting(...)` to cover both branches of a flag without a network. ## Frequently asked questions ### How do I add feature flags to a Go application? Run `go get github.com/canopy-labs/featureflip-go/v2`, then create one client in `main` with `client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY"))`, check the error, and `defer client.Close()`. Read flags anywhere with `client.BoolVariation("your-flag", featureflip.EvaluationContext{UserID: id}, false)`. Evaluation is a synchronous in-memory read, so a flag check adds no latency to a request. The [Go quickstart](/docs/quickstart/go/) covers the minimal setup and the [Go SDK reference](/docs/sdks/go/) documents every option and variation method. ### Does the Featureflip Go SDK require cgo? No. The SDK is pure Go and depends only on the standard library’s `net/http`, with no external modules and no C bindings. You can build with `CGO_ENABLED=0` and get a fully static binary that runs in a `scratch` or distroless image, and cross-compiling with `GOOS`/`GOARCH` works without a C toolchain. Adding feature flags does not change the static-binary deployment Go services are built to ship. ### Is flag evaluation in the Go SDK safe to call from multiple goroutines? Yes. The SDK stores the ruleset behind a `sync.RWMutex`, and the variation methods take a read lock, so `BoolVariation`, `StringVariation`, and the others are safe to call concurrently from any number of goroutines without external synchronization. Each Go HTTP handler runs in its own goroutine, so you can evaluate flags directly in handlers and middleware on a busy endpoint with no added locking. ### Do I need a readiness gate so my Go service doesn’t serve default flag values at startup? Not for correctness. `featureflip.Get()` blocks until the initial flag fetch completes and returns an `error` if it fails or times out, so once it returns a usable client the ruleset is already in memory and the first request evaluates against real configuration. Treat a non-nil error from `Get` as a failed boot (for example with `log.Fatalf`). That is different from an SDK whose client is created asynchronously, where you would otherwise need to await initialization before serving. ### How do feature flags stay consistent across multiple Go instances? Each instance holds its own in-memory ruleset and its own streaming connection, and the same `UserID` hashes to the same bucket on every instance, so a percentage rollout serves the same users the same variation regardless of which replica answers a request. You do not need sticky sessions. A flag flipped in the dashboard pushes to every instance over its stream within milliseconds, so the whole fleet changes together without a redeploy or a shared cache to invalidate. Featureflip is a focused, flat-priced feature flag platform with a pure-Go SDK that evaluates flags locally in memory, so a Go service gets goroutine-safe flag checks on the hot path, fleet-wide updates over streaming, a static binary that still ships to `scratch`, and no bill that scales with request volume. The [Go SDK reference](/docs/sdks/go/) has the full API, the companion guides on [feature flags in Node.js](/blog/feature-flags-in-nodejs/) and [feature flags in Python](/blog/feature-flags-in-python/) cover the same local-evaluation pattern for other backends, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-in-nextjs/ # Next.js Feature Flags: Server-Side Evaluation, No Flash [Blog](/blog/) # Next.js Feature Flags: Server-Side Evaluation, No Flash Next.js feature flags without the hydration flash: evaluate in a Server Component or getServerSideProps, then seed the client hook so it keeps updating live. June 24, 2026 · Updated July 31, 2026 · - nextjs - tutorials Feature flags in a single-page React app are a client-side state problem. Feature flags in Next.js are that, plus a rendering problem the SPA never had: the server renders HTML before the browser has loaded a single flag. Evaluate flags only in the browser and the server paints the default variant, the page hydrates, then the real variant swaps in a beat later. For a button color that is invisible. For gating a whole checkout flow or a paid-tier feature it is a visible flash, and React will warn you about the hydration mismatch in the console. The fix is to evaluate where the markup is generated. This guide wires Next.js feature flags with the right SDK in the right place: the [Node server SDK](/docs/sdks/nodejs/) for server-side evaluation in the App Router and `getServerSideProps`, and the [React SDK](/docs/sdks/react/) for live client updates, wired so the server-rendered value and the first client render always agree. The code covers both the App Router and the Pages Router. **Key Takeaways** - The Next.js-specific problem is the hydration flash: a browser-only flag read paints the default variant on the server, then swaps after flags load. Evaluating on the server puts the correct variant in the initial HTML. - Use two SDKs for two jobs: `@featureflip/node` (a server key, local evaluation) inside Server Components and `getServerSideProps`, and `@featureflip/react` (a client key) for live updates in Client Components. - `FeatureflipClient.get()` dedupes by key, so a module-level server client is safe to import from every request handler without opening duplicate connections. - To keep a flag live without re-introducing the flash, pass the server-evaluated value as the *default* to `useFeatureFlag('key', serverValue)`. The hook returns its default until the client connects, so the first client render matches the server HTML, then streaming takes over. - The server key is read in process and never ships to the browser; only the `NEXT_PUBLIC_` client key reaches the bundle, and it returns pre-evaluated results, not your ruleset. ## 1. The flash a Next.js app has that a React SPA doesn’t In a client-rendered React app, the first paint already happens in the browser, so reading a flag with a hook and gating on a loading state is the whole story. Next.js renders on the server first. That server pass runs before any browser code, so a flag read that only works in the browser has nothing to return yet and falls back to its default. Walk through what the user sees. The server renders the page with `new-checkout` defaulting to `false` and ships HTML for the legacy flow. The browser parses that HTML, hydrates, mounts the Featureflip client, and fetches flags. A few hundred milliseconds later the client learns this user is in the rollout and re-renders the new flow. The user watched the old checkout for a moment and then saw it replaced. Worse, the HTML React rendered on the server (legacy) does not match what the client wants to render on its first pass (new), which is exactly the condition that produces a React hydration warning. Client-only evaluation paints the default first, so a flag whose default differs from the user's variant flashes until the browser loads flags. Evaluating on the server puts the correct variant in the initial HTML. The plain React approach of gating on `useFeatureflipStatus().isReady` and showing a skeleton solves the flash, but in Next.js it means server-rendering a skeleton instead of real content, which throws away the main reason you reached for server rendering. The better answer is to evaluate the flag while the server is building the page. ## 2. Two SDKs, two jobs Featureflip ships a server SDK and a client SDK, and a Next.js app uses both because it runs code in two places. The [Node server SDK](/docs/sdks/nodejs/) (`@featureflip/node`) holds your full ruleset in memory and evaluates flags locally. A `boolVariation()` call is a synchronous in-memory read, not a network request, so evaluating during a server render adds no per-request round trip. It authenticates with a server SDK key, which stays in your server process. The [React SDK](/docs/sdks/react/) (`@featureflip/react`) runs in the browser, opens a streaming connection, and re-renders components when a flag changes while a user sits on the page. It authenticates with a client key, which is safe to ship in the bundle because client evaluation returns only the results a user is allowed to see, never the ruleset. The division of labor is simple. Evaluate on the server to get correct HTML on the first paint. Add the client SDK only where you also want a flag to update live without a reload. Many flags need only the first one. ```bash npm install @featureflip/node # server-side evaluation npm install @featureflip/react @featureflip/browser # client-side live updates ``` ## 3. App Router: evaluate a flag in a Server Component Create one shared server client. `FeatureflipClient.get()` dedupes by SDK key and `create()` waits for the first flag load, so memoizing the promise at module scope gives you a single initialized client for the whole process, safe to import anywhere. lib/featureflip.ts ```ts import { FeatureflipClient } from '@featureflip/node'; let clientPromise: Promise | null = null; export function getFlags() { clientPromise ??= FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, }); return clientPromise; } ``` | null = null;export function getFlags() { clientPromise ??= FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, }); return clientPromise;}"> Now an async Server Component awaits the client and reads the flag for the current user. Evaluation takes a context, so pass whatever identifies the user from your session. app/checkout/page.tsx ```tsx import { getFlags } from '@/lib/featureflip'; import { getSession } from '@/lib/session'; export default async function CheckoutPage() { const flags = await getFlags(); const session = await getSession(); const newCheckout = flags.boolVariation( 'new-checkout', { user_id: session.userId }, false, ); return newCheckout ? : ; } ``` : ;}"> The HTML that leaves the server already contains the correct flow. There is no skeleton, no hydration warning, and nothing to swap once the browser takes over. If `new-checkout` is driven by a [percentage rollout](/docs/concepts/rollout-strategies/) or a [targeting rule](/docs/concepts/targeting/) on the user’s plan, the server resolves it the same way the client would, because both run the same evaluation logic against the same context. Because the variation call is a local read, you can sprinkle these through the tree without worrying about request waterfalls. The one async step is awaiting `getFlags()` once; every `boolVariation` after that is synchronous. ## 4. Pages Router: evaluate in getServerSideProps The Pages Router has no Server Components, but the pattern is the same: evaluate inside `getServerSideProps`, where you already have access to the request, and pass the result down as a prop. pages/checkout.tsx ```tsx import type { GetServerSideProps } from 'next'; import { getFlags } from '../lib/featureflip'; import { getSession } from '../lib/session'; export const getServerSideProps: GetServerSideProps = async ({ req }) => { const flags = await getFlags(); const session = await getSession(req); const newCheckout = flags.boolVariation( 'new-checkout', { user_id: session.userId }, false, ); return { props: { newCheckout } }; }; export default function CheckoutPage({ newCheckout }: { newCheckout: boolean }) { return newCheckout ? : ; } ``` { const flags = await getFlags(); const session = await getSession(req); const newCheckout = flags.boolVariation( 'new-checkout', { user_id: session.userId }, false, ); return { props: { newCheckout } };};export default function CheckoutPage({ newCheckout }: { newCheckout: boolean }) { return newCheckout ? : ;}"> The page renders on the server with the flag already resolved, so the markup is correct before it reaches the browser. This is the same `getFlags()` singleton from the App Router example, which is why keeping the client in one module pays off: both routers share it. ## 5. Keep a flag live without bringing the flash back Server evaluation gives you correct HTML. What it does not give you is live updates: a flag flipped in the dashboard will not reach an already-open page, because the server only ran once, at request time. For most flags that is fine. For an operational kill switch or a flag you flip during an incident, you want the open page to react. The instinct is to add the client provider and read the flag with a hook, but a naive hook read brings the flash straight back, because the hook returns its default until the browser SDK finishes loading. The fix is to seed the hook with the value the server already computed. Put the provider in a Client Component near the root and pass it the same context you used on the server: app/flags-provider.tsx ```tsx 'use client'; import { FeatureflipProvider } from '@featureflip/react'; export function FlagsProvider({ userId, children, }: { userId: string; children: React.ReactNode; }) { return ( {children} ); } ```  {children}  );}"> Then read the flag in a Client Component, passing the server value as the default: app/checkout/checkout-toggle.tsx ```tsx 'use client'; import { useFeatureFlag } from '@featureflip/react'; export function CheckoutToggle({ serverValue }: { serverValue: boolean }) { const newCheckout = useFeatureFlag('new-checkout', serverValue); return newCheckout ? : ; } ``` : ;}"> The Server Component evaluates the flag and hands the result down: ```tsx const newCheckout = flags.boolVariation('new-checkout', { user_id: session.userId }, false); return ; ``` ;"> Now the timing works out. The server renders with the real value. On the client, `useFeatureFlag` returns `serverValue` until the browser SDK connects, so the first client render matches the server HTML exactly, with no mismatch and no flash. Once the [streaming connection](/docs/sdks/browser/) is live, a later flip of `new-checkout` pushes to the open page and the component re-renders on its own. You get a correct first paint and live updates, which is the combination a browser-only setup cannot reach. ## 6. Server keys, client keys, and where evaluation runs Two keys do two things, and keeping them straight is the security model. The server SDK key authorizes local evaluation of the full ruleset. Read it from an un-prefixed environment variable like `FEATUREFLIP_SDK_KEY` so Next.js keeps it on the server and never inlines it into client JavaScript. The client key authorizes pre-evaluated reads for one user and belongs in a `NEXT_PUBLIC_FEATUREFLIP_CLIENT_KEY` variable, which Next.js deliberately exposes to the browser. Shipping the client key is intended; shipping the server key is not. One runtime caveat. The Node SDK uses Node.js APIs, so run it where the Node.js runtime is available, which is the default for Server Components, Route Handlers, and `getServerSideProps`. Next.js middleware runs on the Edge runtime, which does not provide the full Node API surface, so do flag evaluation in a Node.js-runtime route rather than inside middleware. That leaves one case the SDK cannot serve: a flag that decides routing, which has to resolve before the request reaches a route at all. Call the evaluation API over HTTP from the middleware instead. It is one `fetch`, and `fetch` is available on the Edge runtime: middleware.ts ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export async function middleware(request: NextRequest) { const res = await fetch('https://eval.featureflip.io/v1/sdk/evaluate', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FEATUREFLIP_SDK_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ flagKey: 'new-checkout', context: { userId: request.cookies.get('uid')?.value }, }), }) const { value } = await res.json() return value === true ? NextResponse.rewrite(new URL('/checkout-v2', request.url)) : NextResponse.next() } export const config = { matcher: '/checkout' } ``` This path costs a network call on the request hot path, unlike the in-memory reads everywhere else in this guide, so reserve it for routing decisions that genuinely cannot wait. Give the `fetch` a timeout and treat any failure as the control variation, because an evaluation call that hangs on the edge holds up the whole request. Use a server key. Keep it in `FEATUREFLIP_SDK_KEY` with no `NEXT_PUBLIC_` prefix, which is what stops it reaching the browser. Everything downstream of the rewrite still uses local SDK evaluation in your Node.js routes. ## 7. Why a first-party SDK instead of a fetch in getServerSideProps You can skip the SDK and call an HTTP endpoint from the server on every request. It works until the endpoint is slow or down, and now every page render waits on a network call or has to handle that failure. Local evaluation sidesteps that: the SDK loads the ruleset once, keeps it current over a stream, and every `boolVariation` is an in-memory read. The same logic that makes a server-side flag read cheap is the logic you would otherwise hand-build and keep in sync with the dashboard, which is the kind of undifferentiated work a [build-versus-buy decision](/blog/build-vs-buy-feature-flags/) is meant to settle. Pricing matters for a server-rendered app in a specific way. Server evaluation runs once per request and client evaluation runs once per visitor, so a usage-billed platform meters both your traffic and your render volume, and a launch that triples visits triples the bill. Featureflip uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations on every plan, so neither server renders nor browser sessions move the invoice. The [pricing page](/pricing/) has the numbers, and the [comparison pages](/compare/) show how that lands against per-MAU and per-seat vendors for a high-traffic Next.js site. ## The shorter version The thing that makes feature flags in Next.js different from a React SPA is the server render: evaluate a flag only in the browser and the server paints the default, producing a flash and a hydration mismatch. Evaluate on the server instead. Use `@featureflip/node` with a server key inside a Server Component or `getServerSideProps`, reading flags with `boolVariation('key', context, default)` as cheap in-memory calls. That alone gives correct HTML on the first paint for any flag that does not need to change on an open page. When a flag does need to update live, add the `@featureflip/react` provider in a Client Component and pass the server-evaluated value as the hook’s default, so the first client render matches the server HTML and the streaming connection takes over from there. Keep the server key un-prefixed and the client key behind `NEXT_PUBLIC_`, and run the Node SDK in Node.js-runtime routes rather than Edge middleware. ## Frequently asked questions ### How do I add feature flags to a Next.js app? Install `@featureflip/node` and evaluate flags on the server. Create one client with `FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY })`, memoized at module scope, then call `flags.boolVariation('your-flag', { user_id }, false)` inside an async Server Component (App Router) or inside `getServerSideProps` (Pages Router). The page renders with the correct variant already in the HTML. Add the `@featureflip/react` provider only for flags you also want to update live in the browser. ### Why do feature flags flash on the screen in Next.js? Because the server renders before any browser code runs. A flag that is only read in the browser has no value during the server pass, so it falls back to its default, and Next.js ships HTML for that default. When the browser loads flags a moment later, the real variant replaces the default, which both looks like a flash and triggers a React hydration mismatch warning. Evaluating the flag on the server with the Node SDK puts the correct variant in the initial HTML and removes both symptoms. ### Do I use the React SDK or the Node SDK in Next.js? Both, for different jobs. The Node SDK (`@featureflip/node`) evaluates flags on the server in Server Components and `getServerSideProps`, which is what kills the hydration flash. The React SDK (`@featureflip/react`) runs in the browser and gives you live updates over a streaming connection for flags that change while a user is on the page. For a flag that never changes on an open page, the Node SDK alone is enough. ### How do I avoid a hydration mismatch when a flag updates live? Evaluate the flag on the server and pass that value as the default to the client hook: `useFeatureFlag('key', serverValue)`. The hook returns its default until the browser SDK connects, so the first client render uses the same value the server rendered and the markup matches. After the client connects over streaming, it updates the value live. Seeding the hook with the server value is what keeps the first render consistent while still allowing live changes. ### Can I evaluate flags in Next.js middleware or the Edge runtime? The Node SDK targets the Node.js runtime, which is the default for Server Components, Route Handlers, and `getServerSideProps`, so do local evaluation there. Next.js middleware runs on the Edge runtime, which lacks the full Node API surface, so the Node SDK is not the right tool inside middleware. When a flag decides routing and has to resolve before any route runs, `POST` to `https://eval.featureflip.io/v1/sdk/evaluate` from the middleware with a server key and a short timeout, falling back to the control variation on any failure. That is a network call per request, so keep it to routing decisions and leave everything else on local SDK evaluation in your Node.js routes. The worked example is in section 6 above. Featureflip is a focused, flat-priced feature flag platform with a first-party Node SDK for server-side evaluation and a React SDK for live client updates, so a Next.js app gets correct HTML on the first paint and real-time flags after, without metering either on traffic. The [Node SDK reference](/docs/sdks/nodejs/) and the companion guides on [feature flags in React](/blog/feature-flags-in-react/) and [running feature flags in a Node.js backend](/blog/feature-flags-in-nodejs/) have the full API, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-in-nodejs/ # Node.js Feature Flags: Local Evaluation, Long-Lived Server [Blog](/blog/) # Node.js Feature Flags: Local Evaluation, Long-Lived Server Node.js feature flags in a long-lived server: one shared client, local in-memory evaluation, readiness gating on boot, and a clean shutdown on SIGTERM. June 25, 2026 · - nodejs - tutorials Adding a feature flag to a one-off script is easy: read the flag, branch, exit. A Node.js backend is a different shape. It boots once, serves thousands of requests across a process that stays up for days, and usually runs as several identical replicas behind a load balancer. That long life is what makes flags in Node a server problem rather than a snippet problem. You have to decide what happens to requests that arrive before flags have loaded, make sure one process holds exactly one client, keep each flag check off the network so it does not slow a hot endpoint, and flush analytics before the process exits. This guide adds feature flags to a Node.js service with the [Node server SDK](/docs/sdks/nodejs/) , which evaluates flags locally in memory so a check costs about as much as a `Map.get`. The examples use Express, but the pattern is identical for Fastify, Koa, NestJS, or a background worker. For a copy-paste first flag, the [Node.js quickstart](/docs/quickstart/nodejs/) is the five-minute version. This post is about the operational decisions a real server has to make around it. **Key Takeaways** - A Node server evaluates flags locally: the SDK holds the ruleset in memory and `boolVariation()` is a synchronous read, so a flag check adds no network round trip to a request. - Create one client per process. `FeatureflipClient.get()` refcounts by SDK key, so importing a shared module from every route is safe and never opens a second streaming connection. - Gate startup on the first flag load. Await initialization before `app.listen()` and expose it on a readiness probe, or the first requests after boot silently serve default values. - One dashboard flip reaches every replica. Each instance streams config independently and buckets the same `user_id` the same way, so a percentage rollout stays consistent across the fleet with no sticky sessions and no redeploy. - Close the client on `SIGTERM`. `close()` flushes buffered analytics events and shuts the stream down so nothing is lost when the process exits. ## 1. Why a long-lived server changes the problem A flag has three common homes, and the middle one is where most of the work lives. In a one-off script, flags barely register: load the config, read a value, do the work, exit. In a browser tab, the constraint is the render, which is why a [React app](/blog/feature-flags-in-react/) cares about re-renders and a [Next.js app](/blog/feature-flags-in-nextjs/) cares about the hydration flash. A persistent Node.js server sits between those two. It is alive long enough that fetching the ruleset on every request would be wasteful, and busy enough that any per-check latency multiplies across traffic. That is the case local evaluation is built for. The SDK fetches your full ruleset once at startup, keeps it current over a streaming connection, and answers every `boolVariation()` from memory. The long-lived process pays the setup cost a single time and then gets effectively free flag checks for the rest of its life. The flip side is that a long-lived process has a lifecycle you have to respect: a boot window where flags are not loaded yet, a shared client that must not be duplicated, and a shutdown where buffered events still need to leave the process. The rest of this guide walks those in order. ## 2. Install the SDK and create one client for the whole process Install the server package: ```bash npm install @featureflip/node ``` Then create the client once, in its own module, and export it: lib/flags.ts ```ts import { FeatureflipClient } from '@featureflip/node'; export const flags = FeatureflipClient.get({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, }); ``` `FeatureflipClient.get()` returns synchronously and kicks off the configuration fetch in the background. Every route file, service, and DI container can import this one `flags` module. The factory dedupes by SDK key and refcounts the shared core, so even if a library you depend on also calls `get()` with the same key, the process still holds a single SSE connection, a single event-flush loop, and one in-memory store. The thing to avoid is constructing a client per request. There is no public constructor for exactly this reason, but the principle is broader than the API: a Featureflip client owns a long-lived streaming connection and background timers, and one per request would open thousands of them. One process, one client. The [Lifetime section](/docs/sdks/nodejs/#lifetime) of the SDK reference has the full refcounting contract if you run a multi-tenant process that serves more than one environment key. ## 3. Don’t answer requests before flags have loaded Because `get()` returns immediately and loads flags in the background, there is a window at boot where the client is alive but has no configuration. A `boolVariation()` call in that window returns its default value. If your server starts listening during that window, the first users to hit it can land on the “off” path for a feature that is actually on for them, with no error to tell you it happened. Close the window in two places. First, await initialization before you bind the port: server.ts ```ts import express from 'express'; import { flags } from './lib/flags'; const app = express(); // ... routes ... async function start() { await flags.waitForInitialization(); app.listen(3000, () => console.log('flags loaded, accepting traffic')); } start(); ``` console.log('flags loaded, accepting traffic'));}start();"> Second, expose readiness so an orchestrator never routes traffic to a pod that is still booting: ```ts app.get('/readyz', (_req, res) => { res.status(flags.isInitialized ? 200 : 503).send(); }); ``` { res.status(flags.isInitialized ? 200 : 503).send();});"> `waitForInitialization()` resolves once the first configuration fetch completes and rejects if it has not finished within `initTimeout` (10 seconds by default), so a failed fetch surfaces as a failed boot rather than a silently wrong server. On Kubernetes, pointing a readiness probe at `/readyz` keeps the pod out of the load balancer rotation until flags are in memory, so a rolling deploy never sends a request to an instance that would serve defaults. ## 4. Keep every flag check off the network Once flags are loaded, evaluation is a synchronous in-memory read. A variation call takes no `await`, makes no request, and cannot time out: ```ts app.get('/dashboard', (req, res) => { const newNav = flags.boolVariation( 'new-dashboard-nav', { user_id: req.user.id, plan: req.user.plan }, false, ); res.render(newNav ? 'dashboard-v2' : 'dashboard-v1'); }); ``` { const newNav = flags.boolVariation( 'new-dashboard-nav', { user_id: req.user.id, plan: req.user.plan }, false, ); res.render(newNav ? 'dashboard-v2' : 'dashboard-v1');});"> The third argument is the default, returned if the flag is missing or evaluation fails, so a flag check has no failure path of its own to handle. A [targeting rule](/docs/concepts/targeting/) on `plan` or a [percentage rollout](/docs/concepts/rollout-strategies/) resolves locally exactly as it would anywhere else, because the SDK runs the same evaluation logic against the context you pass. The practical payoff is that you can scatter flag checks through middleware, route handlers, and deep service functions without thinking about request waterfalls. The alternative, calling a flag API over HTTP on each check, puts a network round trip on the hot path and a failure mode in every branch. Calling a flag service over the network bills a round trip to every request that checks a flag. Local evaluation answers from an in-memory copy of the ruleset, so the cost is a synchronous read and flag checks are effectively free on the hot path. ## 5. Flip a flag across every replica at once Production Node services rarely run as one process. They run as several replicas behind a load balancer, and a request for a given user can land on any of them. Local evaluation handles this without a shared cache to invalidate. Each instance holds its own in-memory copy of the ruleset and keeps its own streaming connection open. When you flip a flag in the dashboard, the change pushes over that stream to every instance within milliseconds. There is nothing to redeploy and no cache TTL to wait out. Consistency across instances comes from deterministic bucketing: the same `user_id` hashes to the same position in a [percentage rollout](/glossary/percentage-rollout/) on every replica, so a 10% rollout is the same 10% of users no matter which pod answers a given request. You do not need sticky sessions to keep a user on one side of a rollout. This is the property that makes flags useful as an operational control on a backend, not just a release toggle. Flip a [kill switch](/glossary/kill-switch/) during an incident and every instance honors it within milliseconds. If a streaming connection drops, the SDK keeps serving the last known configuration while it reconnects, and falls back to interval polling after repeated failures, so a network blip degrades to slightly staler flags rather than to defaults. ## 6. Flush events and shut down cleanly The SDK batches analytics events, the exposure data behind a [percentage rollout](/docs/concepts/rollout-strategies/) and any `track()` calls you make, and flushes them on an interval. A process that exits without closing the client loses whatever is still in the current batch. On a server that redeploys often, that is a steady trickle of dropped events. Close the client on the termination signal: ```ts process.on('SIGTERM', async () => { await flags.close(); process.exit(0); }); ``` { await flags.close(); process.exit(0);});"> `close()` flushes buffered events, closes the SSE connection, and stops the background timers. Under the refcount model, the shared core only runs its real shutdown when the last handle for that SDK key closes, so closing your one shared client on `SIGTERM` is exactly right. Calling `close()` twice is a no-op, so a defensive double-close in a shutdown handler does no harm. ## 7. Test flag-gated code without a network Flag-gated branches need tests on both sides, and you do not want those tests reaching the network or standing up a server. `FeatureflipClient.forTesting()` returns a standalone client with hardcoded values, registered nowhere in the factory cache and opening no connections: ```ts import { FeatureflipClient } from '@featureflip/node'; const flags = FeatureflipClient.forTesting({ 'new-dashboard-nav': true, }); flags.boolVariation('new-dashboard-nav', {}, false); // true ``` Inject the test client wherever your code expects the real one and assert on each branch independently. Because the client is fully initialized the moment it is created and ignores the evaluation context, there is no init step to await and no fetch to mock. ## 8. Why a first-party SDK, and what it meters You can skip the SDK and call a flag endpoint over HTTP, or poll it and cache the result yourself. That works until the endpoint is slow or down, at which point every request that checks a flag is waiting on a network call or handling its failure, and you are now maintaining the cache, the refresh loop, and the reconnection logic by hand. Local evaluation is that machinery, built and kept in sync with the dashboard for you, which is the kind of undifferentiated plumbing a [build-versus-buy decision](/blog/build-vs-buy-feature-flags/) usually settles in favor of buying. Pricing matters for a backend in a specific way. A server evaluates flags once per request, so on a platform that bills by evaluation or by monthly active user, your flag bill tracks your traffic, and a launch that triples requests triples the meter. Featureflip uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations on every plan, so request volume never moves the invoice. The [pricing page](/pricing/) has the numbers, and the [comparison pages](/compare/) show how that lands against per-MAU and usage-billed vendors for a high-traffic service. ## The shorter version What makes feature flags in Node.js different from a script or a browser is the long-lived process. Install `@featureflip/node` and create one client with `FeatureflipClient.get({ sdkKey })` in a shared module, so the whole process uses a single streaming connection and in-memory store. Await `waitForInitialization()` before `app.listen()` and surface `isInitialized` on a readiness probe, so the server never answers a request before flags have loaded. Evaluate with `boolVariation('key', context, default)`, which is a synchronous in-memory read with no network round trip, safe to call anywhere on the hot path. Across replicas, each instance streams config independently and buckets users deterministically, so a dashboard flip reaches the whole fleet in milliseconds with no redeploy. Close the client on `SIGTERM` to flush analytics, and use `FeatureflipClient.forTesting()` to cover both branches of a flag without a network. ## Frequently asked questions ### How do I add feature flags to a Node.js app? Install `@featureflip/node` and create one client in a shared module with `FeatureflipClient.get({ sdkKey: process.env.FEATUREFLIP_SDK_KEY })`. Await `flags.waitForInitialization()` before your server starts listening, then read flags anywhere with `flags.boolVariation('your-flag', { user_id }, false)`. Evaluation is a synchronous in-memory read, so a flag check adds no latency to a request. The [Node.js quickstart](/docs/quickstart/nodejs/) covers the minimal setup and the [SDK reference](/docs/sdks/nodejs/) covers every option. ### Does evaluating a feature flag in Node.js make a network call? No. The Node SDK fetches your full ruleset once at startup and keeps it current over a streaming connection, then evaluates every flag locally from memory. A `boolVariation()` call costs about as much as a `Map.get`, with no per-request round trip to Featureflip and no failure path on the variation call itself. The only network activity is the initial load and the background stream, both of which happen outside your request handlers. ### How many Featureflip clients should I create in one Node.js process? One. `FeatureflipClient.get()` refcounts by SDK key, so every call with the same key returns a handle backed by a single shared core, one SSE connection, one event-flush loop, one in-memory store. Create the client once in a shared module and import it everywhere, including from per-request handlers and DI containers. Avoid constructing a client per request, which is why the SDK has no public constructor. ### How do I avoid serving default flag values when my Node.js server starts up? `FeatureflipClient.get()` returns before flags have loaded, so await `flags.waitForInitialization()` before calling `app.listen()`, and expose `flags.isInitialized` on a readiness endpoint like `/readyz`. Awaiting init means the process does not accept traffic until the ruleset is in memory, and the readiness probe keeps an orchestrator from routing requests to a pod that is still booting. Together they close the window where a flag check would return its default. ### How do feature flags stay consistent across multiple Node.js instances? Each instance holds its own in-memory ruleset and its own streaming connection, and the same `user_id` hashes to the same bucket on every instance, so a percentage rollout serves the same users the same variation regardless of which replica answers a request. You do not need sticky sessions. A flag flipped in the dashboard pushes to every instance over its stream within milliseconds, so the whole fleet changes together without a redeploy or a shared cache to invalidate. Featureflip is a focused, flat-priced feature flag platform with a first-party Node SDK that evaluates flags locally in memory, so a Node.js backend gets fast flag checks on the hot path, fleet-wide updates over streaming, and no bill that scales with request volume. The [Node SDK reference](/docs/sdks/nodejs/) has the full API, the companion guide on [feature flags in Next.js](/blog/feature-flags-in-nextjs/) covers the server-render case, [adding flags to a Go service](/blog/feature-flags-in-go/) applies the same local-evaluation pattern to a static Go binary, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-in-python/ # Python Feature Flags: A Fork-Safe Gunicorn and uWSGI Setup [Blog](/blog/) # Python Feature Flags: A Fork-Safe Gunicorn and uWSGI Setup Python feature flags without the pre-fork trap: local in-memory evaluation, why Gunicorn and uWSGI silently freeze flags, and where to build the client. June 29, 2026 · Updated July 31, 2026 · - python - tutorials Most feature flag tutorials stop at “read a flag, branch on it.” In Python that part is three lines. The part that actually decides whether flags work in production is where you build the client, because nearly every Python web app runs behind a pre-forking server: Gunicorn, uWSGI, or Uvicorn with several workers. Those servers fork worker processes, and a background thread does not survive a fork. A flag client created in the wrong place starts a streaming thread that exists in the parent and is gone in every worker, so the workers serve whatever flag values were frozen at fork time, forever, with no error to tell you. The rest of this guide is about putting the client where that does not happen. This guide wires Python feature flags into a real service with the [Python server SDK](/docs/sdks/python/) , which evaluates flags locally in memory so a check is a synchronous in-process read with no network round trip. For a copy-paste first flag, the [Python quickstart](/docs/quickstart/python/) is the five-minute version. This post covers the operational decision that quickstart leaves open: how a long-lived, pre-forked Python service has to be wired so flags keep updating in every worker. **Key Takeaways** - A Python web app almost always runs behind a pre-forking server (Gunicorn, uWSGI, Uvicorn workers). Worker processes are created with `os.fork()`, and only the forking thread is copied into the child. - The SDK keeps flags current on a background thread. If the client is built before the fork (for example under Gunicorn `--preload`), that thread is absent in every worker, so flags freeze at their fork-time values and never update again. No exception is raised. - The fix is to build the client *after* the fork: a Gunicorn `post_fork` hook, a uWSGI `@postfork` function, a FastAPI `lifespan`, or lazy first-use construction. The SDK’s per-process cache makes lazy construction safe. - `client.variation("key", {"user_id": id}, default)` is a synchronous in-memory read guarded by a lock, so it is safe from threaded WSGI workers and fine to call from an async view without `await`. - Across workers and replicas the same `user_id` buckets the same way, so a percentage rollout is consistent without sticky sessions, and a dashboard flip reaches every worker over its own stream. ## 1. What makes feature flags in Python different A flag lives in one of three very different homes, and the Python web case sits at the awkward end. In a one-off script or a notebook, flags barely register: build the client, read a value, do the work, exit. In a browser tab the constraint is the render, which is why a [React app](/blog/feature-flags-in-react/) cares about avoiding extra re-renders and a [Next.js app](/blog/feature-flags-in-nextjs/) cares about the hydration flash. A Python web service is the third home, and its defining property is not the language, it is the deployment: the process you wrote is not the process that serves requests. A WSGI or ASGI server boots, then forks a pool of worker processes, and your request handlers run in those children. That fork is the thing a feature flag library has to respect. Local evaluation wants a long-lived process that fetches the ruleset once, keeps it current over a streaming connection, and answers every check from memory. That model is the same one the [Node.js server guide](/blog/feature-flags-in-nodejs/) and the [Go service guide](/blog/feature-flags-in-go/) describe. The Python twist is that “the long-lived process” is the worker, not the master that imported your code, and the streaming connection is a thread that the fork does not carry across. Get the client into the worker and everything works. Build it in the master and the workers inherit a snapshot with no way to refresh it. The next two sections are that distinction in full. ## 2. Install the SDK and evaluate locally Install the package: ```bash pip install featureflip ``` The SDK targets Python 3.10+ and is synchronous on purpose: `variation()` returns a value, not a coroutine, so it drops into existing Django, Flask, FastAPI, or Celery code without introducing `asyncio`. Building a client fetches your full ruleset over HTTPS and blocks until that fetch finishes: ```python from featureflip import FeatureflipClient client = FeatureflipClient(sdk_key="your-sdk-key") ``` Because the constructor blocks on the initial load and raises `InitializationError` on timeout, a client that hands back successfully already has flags in memory, so the first request it serves evaluates against real configuration. Hold the key in `FEATUREFLIP_SDK_KEY` and you can drop the argument; the SDK reads the environment variable. Evaluation is a single method whose default argument decides the type: ```python show_banner = client.variation("new-banner", {"user_id": "u-8a31"}, default=False) limit = client.variation("rate-limit", {"user_id": "u-8a31"}, default=100) color = client.variation("banner-color", {"user_id": "u-8a31"}, default="blue") ``` There is no separate `bool_variation` or `string_variation`. Pass a `bool` default and you get a `bool` back; pass an `int`, a `str`, or a dict and the return type follows. The `default` is returned if the flag is missing or evaluation fails, so a flag check has no error path of its own. When you need to know *why* a value came back, `client.variation_detail("new-banner", ctx, default=False)` returns an `EvaluationDetail` carrying the `reason` and the matched `rule_id`. The [Python quickstart](/docs/quickstart/python/) has the minimal end-to-end version; the rest of this guide is about the part it does not cover. ## 3. The pre-fork trap: why a client built too early goes silently stale Here is the setup that looks correct and is not. You construct the client at import time, or in `create_app()`, and you run under Gunicorn with `--preload` so the app loads once in the master before workers fork: ```python # app.py: built at import, then the master is forked under --preload from featureflip import FeatureflipClient client = FeatureflipClient(sdk_key="...") # threads start HERE, in the master ``` The constructor runs in the master. It fetches the ruleset and starts a background thread that holds the streaming connection (plus a second thread that flushes analytics events). Then Gunicorn calls `os.fork()` for each worker. A fork copies the calling thread and nothing else: every other thread in the process is absent in the child. So each worker inherits the flag dictionary exactly as it stood at fork time, but the thread that was keeping it current is gone. The worker evaluates flags happily against a snapshot that can never change. Flip a [kill switch](/glossary/kill-switch/) in the dashboard during an incident and the workers do not hear about it. The event-flush thread is gone too, so exposure and `track` events pile up in the worker’s queue and never ship. Nothing throws. The only symptom is flags that quietly stopped responding. The SDK’s per-process cache does not save you here, and it is worth being precise about why. That cache makes *duplicate construction* harmless: build two clients with the same key in one process and you share one connection. What no cache can fix is a client whose threads were started in a different process. The fork already happened; the threads are already gone. The cache prevents a redundant client, not a dead one. A background thread is not copied into a forked child. Build the client in the master and the workers get a frozen snapshot; build it after the fork and each worker owns a live streaming connection. This is not a Gunicorn-only problem. uWSGI preforks by default: it loads your app in the master and forks workers unless you set `lazy-apps = true`. The same trap applies to any code that opens a connection or starts a thread at import under a preloading server, which is why database drivers and metrics libraries carry the same warning. ## 4. Initialize after the fork, not before The rule is one line: construct the client inside a per-worker startup hook, never at import time under a preloading server. Each server gives you the hook. Under **Gunicorn**, use the `post_fork` server hook in your config file. It runs once inside each worker right after the fork: gunicorn.conf.py ```python def post_fork(server, worker): import myapp.flags myapp.flags.init_client() # builds the client in THIS worker ``` Under **uWSGI**, the `@postfork` decorator does the same job: ```python from uwsgidecorators import postfork @postfork def init_flags(): import myapp.flags myapp.flags.init_client() ``` Under **FastAPI or any ASGI app run with Uvicorn workers**, a `lifespan` context manager runs once per worker process at startup, which is already after the fork: ```python from contextlib import asynccontextmanager from fastapi import FastAPI from featureflip import FeatureflipClient @asynccontextmanager async def lifespan(app: FastAPI): app.state.flags = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) yield app.state.flags.close() app = FastAPI(lifespan=lifespan) ``` The most portable option, which works the same under every server because it defers construction until the first request a worker handles, is lazy first-use: myapp/flags.py ```python import os from featureflip import FeatureflipClient _client: FeatureflipClient | None = None def get_client() -> FeatureflipClient: global _client if _client is None: _client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) return _client ``` FeatureflipClient: global _client if _client is None: _client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) return _client"> The first `get_client()` call in a worker builds the client in that worker, after the fork, so its streaming thread lives where the requests do. This is where the per-process cache earns its keep: if two threads in the same worker race the first call, they share one underlying client and one connection rather than opening two. The SDK reference covers the [lifetime and refcount model](/docs/sdks/python/#lifetime) in full. The single discipline to keep is that nothing builds the client at import time when the app is preloaded; defer it to the worker. ## 5. Where the client goes in Django and Flask Both frameworks hand you an obvious startup hook, and whether that hook is safe has nothing to do with the framework. It comes down to one Gunicorn flag. In Django the natural home is `AppConfig.ready()`, which runs during `django.setup()` when the WSGI module is first imported: myapp/apps.py ```python from django.apps import AppConfig class MyAppConfig(AppConfig): name = "myapp" def ready(self): import myapp.flags myapp.flags.init_client() ``` Launch that as `gunicorn myproject.wsgi` and every worker imports the module for itself, so `ready()` fires once inside each worker after the fork and the setup is correct. Add `--preload` and the master imports it a single time before forking anything, so `ready()` runs in the master and each worker inherits a client whose streaming thread stayed behind. Nothing about the application code changed between those two runs, only the launch flag. Flask has the same shape with an app factory: myapp/__init__.py ```python def create_app(): app = Flask(__name__) app.config["FLAGS"] = get_client() # correct only if this runs per worker return app ``` `gunicorn "myapp:create_app()"` calls the factory inside each worker. `gunicorn --preload "myapp:create_app()"` calls it once in the master. Flask’s old answer to first-request setup, `before_first_request`, was deprecated in 2.2 and removed in 2.3, so it is not available as an escape hatch on a current version. What settles the question is whether your startup hook runs before or after the fork, and `--preload` is what moves it. One line will tell you. Log the pid where the client gets built and compare it against the pid serving a request: ```python import os print("client built in pid", os.getpid()) ``` Two different pids mean the client lives in the wrong process. Drop `--preload`, move construction into the `post_fork` hook above, or defer it with the lazy `get_client()`, which stays correct under every combination of server and flag. ## 6. Evaluate from threaded workers and async views Once a worker holds a client, evaluation is a synchronous in-memory read. The flag store sits behind a lock, so a variation call is safe to make from any thread: ```python def dashboard_view(request): ctx = {"user_id": request.user.id, "plan": request.user.plan} if get_client().variation("new-dashboard-nav", ctx, default=False): return render_dashboard_v2(request) return render_dashboard_v1(request) ``` That covers the two common worker models. Gunicorn and uWSGI can run several threads inside each worker process, and the lock makes concurrent `variation()` calls from those threads safe with no work on your side. A Celery prefork pool is the same picture: build the client per worker in the `worker_process_init` signal, then evaluate from tasks. Because the hot path is a dictionary lookup rather than JSON parsing, the GIL is not a contention point in practice. Async views are fine too. `variation()` does no I/O, so calling it from an `async def` handler without `await` does not block the event loop in any meaningful way; it returns in microseconds from memory. The one call that blocks is the constructor, which is why it belongs in the `lifespan` startup and not inside a request. The context dictionary carries what your rules run against: `user_id` drives percentage rollouts, and any other keys feed [targeting rules](/docs/concepts/targeting/) . ```python ctx = {"user_id": "u-8a31", "plan": "pro", "country": "US"} if get_client().variation("priority-queue", ctx, default=False): route_to_fast_lane() ``` Production Python services run as many worker processes across several replicas, and a request for a given user can land on any of them. Local evaluation handles that without a shared cache to invalidate. Each worker holds its own in-memory ruleset and its own streaming connection, and bucketing is deterministic: the same `user_id` hashes to the same position in a [percentage rollout](/glossary/percentage-rollout/) in every worker and every replica, so a 10% rollout is the same 10% of users no matter which process answers. You do not need sticky sessions. When you flip a flag, the change pushes to each worker’s stream within milliseconds. If a stream drops, the SDK keeps serving the last known values while it reconnects and falls back to polling every 30 seconds, so a network blip degrades to slightly staler flags rather than to defaults. ## 7. Shut down each worker cleanly The client runs background threads and batches analytics events, the exposure data behind a rollout plus any `client.track(...)` calls. On worker shutdown two things have to happen: the pending event batch flushes and the threads stop. Both are `client.close()`, and the cleanest way to guarantee it is the context manager, which closes on exit: ```python with FeatureflipClient(sdk_key="...") as client: value = client.variation("flag", {"user_id": "u-1"}, default=False) # close() runs here: flushes events, stops the background thread ``` For a long-lived server you usually want the client to live for the whole worker, so close it from the same per-worker lifecycle you built it in: the `yield`-and-after half of a FastAPI `lifespan`, a Gunicorn `worker_exit` hook, or a uWSGI shutdown handler. `close()` is refcounted and idempotent, so a defensive double-close is a no-op, and when several handles share one cached client the real shutdown runs only when the last one closes. ## 8. Test flag-gated code with for_testing A flag introduces two branches, and both need tests that do not reach the network. `FeatureflipClient.for_testing` returns a client backed by fixed values, with no HTTP and no threads: ```python def test_checkout_new_flow(): client = FeatureflipClient.for_testing({ "new-checkout-flow": True, "rate-limit": 500, }) ctx = {"user_id": "test-user"} assert client.variation("new-checkout-flow", ctx, default=False) is True ``` The test client ignores the context and returns its configured values, so there is no init to await and nothing to mock. Inject it wherever your code expects a `FeatureflipClient`, and flip the map in a second test to cover the off path. `client.set_test_value("new-checkout-flow", False)` updates a value mid-test when you want both branches in one function. ## 9. Why a first-party SDK, and what it meters You could skip the SDK and call a flag endpoint over HTTP on each request, or poll it and cache the result yourself. That holds up until the endpoint is slow or down, at which point every request that checks a flag is either waiting on a network call or handling its failure, and you now own the cache, the refresh loop, the SSE reconnection, and the fork-safe lifecycle by hand. Local evaluation is that machinery built once and kept in sync with the dashboard for you, the kind of undifferentiated plumbing a [build-versus-buy decision](/blog/build-vs-buy-feature-flags/) usually settles in favor of buying. Pricing lands on a Python backend in a specific way. A service evaluates flags on nearly every request, so on a platform that bills by evaluation or by monthly active user, the flag bill tracks traffic, and a launch that triples requests triples the meter. Featureflip uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations on every plan, so request volume never moves the invoice. The [pricing page](/pricing/) has the numbers, and the [comparison pages](/compare/) show how that lands against per-MAU and usage-billed vendors for a high-throughput service. ## The shorter version What makes feature flags in Python different from a script or a browser is the deployment: a pre-forking server runs your handlers in worker processes, and a background thread does not survive `os.fork()`. Install the SDK with `pip install featureflip`, but do not build the client at import time under a preloading server. Build it after the fork, in a Gunicorn `post_fork` hook, a uWSGI `@postfork` function, a FastAPI `lifespan`, or a lazy `get_client()` first-use helper, so each worker owns a live streaming connection instead of a frozen snapshot. Evaluate with `client.variation("key", {"user_id": id}, default)`, a synchronous in-memory read guarded by a lock that is safe from threaded workers and fine to call from an async view without `await`. Across workers and replicas the same `user_id` buckets the same way, so rollouts are consistent without sticky sessions, and a dashboard flip reaches every worker over its stream. Close the client from the same per-worker lifecycle to flush analytics, and use `FeatureflipClient.for_testing(...)` to cover both branches without a network. ## Frequently asked questions ### How do I add feature flags to a Python application? Run `pip install featureflip`, then build one client per worker process with `FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"])` and read flags with `client.variation("your-flag", {"user_id": id}, default=False)`. Evaluation is a synchronous in-memory read, so a flag check adds no latency to a request. The one rule specific to Python is where you build the client: under a pre-forking server, construct it after the fork (see below), not at import time. The [Python quickstart](/docs/quickstart/python/) covers the minimal setup and the [Python SDK reference](/docs/sdks/python/) documents every option. ### Why do my feature flags stop updating under Gunicorn or uWSGI? Because the client was built before the worker fork. The SDK keeps flags current on a background thread, and `os.fork()` copies only the calling thread, so that thread is absent in each worker and the flag store freezes at its fork-time values with no error. It happens when you construct the client at import time under Gunicorn `--preload` or uWSGI’s default prefork mode. Build the client after the fork instead: a Gunicorn `post_fork` hook, a uWSGI `@postfork` function, a FastAPI `lifespan`, or lazy first-use construction inside the worker. ### Is the Featureflip Python SDK safe to call from multiple threads? Yes. The flag store is guarded by a lock and `variation()` takes it for a read, so calls are safe from threaded WSGI workers, a Celery prefork pool, or any other threads in the process. The hot path is a dictionary lookup rather than JSON parsing, so the GIL is not a contention point. Build the client once per worker process and share that single instance across the worker’s threads. ### Can I use the Python SDK in an async framework like FastAPI? Yes. `variation()` is synchronous and does no I/O, so calling it from an `async def` handler without `await` returns from memory in microseconds and does not block the event loop. Build the client in a `lifespan` context manager, which runs once per worker at startup (after the fork), and close it in the same lifespan on shutdown. The only blocking call is the constructor, which is why it belongs in startup and not inside a request handler. ### How do feature flags stay consistent across multiple Python workers and replicas? Each worker process holds its own in-memory ruleset and its own streaming connection, and the same `user_id` hashes to the same bucket in every worker, so a percentage rollout serves the same users the same variation regardless of which process or replica answers. You do not need sticky sessions. A flag flipped in the dashboard pushes to every worker over its stream within milliseconds, so the whole fleet changes together without a redeploy or a shared cache to invalidate. Featureflip is a focused, flat-priced feature flag platform with a synchronous Python SDK that evaluates flags locally in memory, so a Python service gets thread-safe flag checks on the request path, fleet-wide updates over streaming, and no bill that scales with traffic, as long as you build the client where its background thread can live: inside the worker, after the fork. The [Python SDK reference](/docs/sdks/python/) has the full API, the companion guide on [feature flags in Go](/blog/feature-flags-in-go/) covers the same local-evaluation pattern for a Go backend, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-in-react/ # React Feature Flags: Provider, Hooks, and Live Updates [Blog](/blog/) # React Feature Flags: Provider, Hooks, and Live Updates React feature flags with a provider and a useFeatureFlag hook that re-renders on change, plus user targeting and tests that never touch the network. June 24, 2026 · - react - tutorials Adding a feature flag to a React app is mostly a state-management problem wearing a release-management costume. A flag value lives on a server, it can change while a user is sitting on the page, and when it changes you want exactly the components that read it to re-render with the new value. Do that by hand and you end up writing a fetch, a context, an effect, and a subscription. Do it with a first-party hook and it is two lines: wrap your tree in a provider, read the flag with `useFeatureFlag`. This guide builds up the idiomatic version with the [Featureflip React SDK](/docs/sdks/react/) : install and provider, reading a boolean flag, handling the loading state so users never see the wrong variant flash, multivariate flags, targeting a logged-in user in real time, and testing components without touching the network. The code targets React 18 and React 19. **Key Takeaways** - Feature flags in React are external state: wrap your app in `` once, then call `useFeatureFlag('flag-key', defaultValue)` in any component and it re-renders automatically when the flag changes. - The type of the default value picks the variation method, so `useFeatureFlag('key', false)` reads a boolean, `useFeatureFlag('key', 'blue')` reads a string, and an object default reads JSON. - Gate render on `useFeatureflipStatus().isReady` to avoid the “flash of the wrong variant” before flags load. - A hand-rolled context re-renders every consumer on any change; the SDK uses `useSyncExternalStore` with a memoized snapshot, so only the component reading the changed flag re-renders. - The React SDK is a client-side SDK, billed on Featureflip’s flat plans with unlimited evaluations, so client-side traffic does not meter the bill the way per-MAU pricing does. ## 1. Why feature flags are awkward in React The reason rolling your own flag layer feels harder than it should is that React components are a render tree and flags are server state that mutates underneath that tree. The naive version usually looks like a context holding a flags object, hydrated by a `useEffect` that fetches on mount. It works until you notice three things. It tears and goes stale. A single context value holding every flag means any update replaces the object reference, so every component consuming that context re-renders even if the one flag it reads did not change. You either accept the over-rendering or start hand-writing per-flag selectors and memoization. It misses live changes. A fetch on mount reads the flag once. If you flip a flag during an incident, nothing on an open page reacts until the next reload, unless you also build a polling loop or wire up a server-sent-events stream yourself and merge the deltas into state. It fights React’s own machinery. React 18’s concurrent rendering can read external state at surprising moments, and StrictMode intentionally double-mounts effects in development. A naive `useEffect` + `useState` subscription can open two connections, miss the initial value, or read a torn snapshot. The supported answer to “subscribe a component to an external store” is `useSyncExternalStore`, and wiring it correctly per flag is exactly the boilerplate a first-party hook exists to delete. The SDK closes all three: one provider owns the connection, updates arrive over a stream, and the hook reads through `useSyncExternalStore` with a memoized snapshot so re-renders stay surgical. A single shared context replaces its value on every change, so all consumers re-render. The hook's memoized per-flag snapshot re-renders only the component whose value actually changed. ## 2. Install the SDK and wrap your app in a provider The React SDK is a thin set of bindings over the framework-agnostic [Browser SDK](/docs/sdks/browser/) , so you install both. ```bash npm install @featureflip/react @featureflip/browser ``` Wrap your component tree once, at the root, in `FeatureflipProvider`. The provider creates a client, opens the connection on mount, and cleans up on unmount. ```tsx import { FeatureflipProvider } from '@featureflip/react'; export function App() { return ( ); } ```    );}"> The `clientKey` is a client SDK key, not a server key. Client keys are safe to ship in a browser bundle because client-side evaluation returns only the results a user is allowed to see, never your full ruleset. If you have not created a project and grabbed a key yet, the [JavaScript quickstart](/docs/quickstart/javascript/) walks through it end to end in a few minutes. One provider per app is enough. If you mount two providers with the same `clientKey` (a micro-frontend, say), they share a single underlying connection rather than opening two, so there is no penalty for nesting. ## 3. Read a flag with useFeatureFlag Inside the provider, any component reads a flag with one hook. Pass the flag key and a default value to fall back to before flags load or if the key is missing. ```tsx import { useFeatureFlag } from '@featureflip/react'; function Checkout() { const newFlow = useFeatureFlag('new-checkout', false); return newFlow ? : ; } ``` : ;}"> That is the whole integration for a boolean gate. When the flag flips in the dashboard, or when a [percentage rollout](/docs/concepts/rollout-strategies/) starts serving this user the new variant, `Checkout` re-renders with the new value on its own. There is no subscription to set up and no event listener to remember to remove. The hook handles the subscribe and unsubscribe through `useSyncExternalStore`, and because the snapshot is memoized per flag, a component reading `new-checkout` does not re-render when some other flag changes around it. The full hook and provider API lives in the [React SDK reference](/docs/sdks/react/) . ## 4. Handle the loading state so the wrong variant never flashes There is a window between mount and the first flag load. During that window every `useFeatureFlag` call returns its default. If your default is “feature off” and the user should see “feature on,” they get a flash of the old UI before it swaps. For a button color that is harmless. For swapping an entire checkout flow it looks broken. Gate the part of the tree that depends on flags on the client being ready. `useFeatureflipStatus` exposes that. ```tsx import { useFeatureflipStatus } from '@featureflip/react'; function StatusGate({ children }: { children: React.ReactNode }) { const { isReady, isError, error } = useFeatureflipStatus(); if (isError) return ; if (!isReady) return ; return <>{children}; } ``` ; if (!isReady) return ; return <>{children};}"> Wrap the flag-dependent subtree in the gate and it renders a skeleton until real values are in, then renders once with the correct variant. ```tsx ```    "> How wide to draw the gate is a judgment call. Gate too much and the whole page waits on flags; gate too little and a flagged region pops in late. A good rule is to gate only the subtree whose default-vs-loaded difference is visually jarring, and let cosmetic flags resolve in place. ## 5. Multivariate flags: strings, numbers, and JSON Not every flag is a boolean. A flag can serve one of several string variants, a numeric tuning value, or a whole config object. `useFeatureFlag` reads all of them through the same call: the type of the default value decides which variation is read. ```tsx const enabled = useFeatureFlag('new-checkout', false); // boolean const color = useFeatureFlag('cta-color', 'blue'); // string const limit = useFeatureFlag('upload-limit', 100); // number const layout = useFeatureFlag('dashboard-config', { // JSON object columns: 2, density: 'comfortable', }); ``` The boolean default reads a boolean variation, the string default reads a string variation, the number default reads a number, and an object or array default reads a JSON variation. There is no separate `useStringFlag` or `useJsonFlag` to remember. One caution with object defaults: the hook serializes the value to keep its reference stable across renders, so a returned object only changes identity when its contents actually change. That means you can safely put a flag-driven config object straight into a dependency array without triggering an effect on every render. ## 6. Target a logged-in user in real time Flags get interesting once they evaluate against who the user is: roll out to internal staff first, gate a plan-tier feature, ship to one beta cohort. That needs an evaluation context, and in a React app the context usually arrives after the initial render, once auth resolves. The provider takes a `context` prop. When it changes, the SDK re-identifies the user and re-evaluates every flag, and your components re-render with the user-specific values. ```tsx function App() { const { user } = useAuth(); return ( ); } ```      );}"> Before login the context is `undefined` and flags evaluate anonymously; the moment `user` populates, the provider calls `identify` and a flag with a [targeting rule](/docs/concepts/targeting/) on `plan === 'pro'` starts returning the pro variant without a reload. If you would rather drive identify imperatively, for example inside a login handler, `useFeatureflipClient` hands you the client to call `client.identify({ ... })` directly. Either way, updates also flow the other direction: with streaming on, a flag change made in the dashboard pushes to open sessions over the stream and the relevant components re-render live. ## 7. Test components without the network A flag-gated component should be testable without standing up a server or stubbing fetch. The SDK ships a `TestFeatureflipProvider` that supplies fixed flag values synchronously, with no network call and no loading delay. ```tsx import { TestFeatureflipProvider } from '@featureflip/react'; import { render, screen } from '@testing-library/react'; test('shows the new checkout when the flag is on', () => { render( , ); expect(screen.getByText('Place order')).toBeInTheDocument(); }); ``` { render(   , ); expect(screen.getByText('Place order')).toBeInTheDocument();});"> The test provider reports `isReady: true` immediately, so a `StatusGate` resolves on the first render and you assert on the real variant. Write one test per branch by flipping the value in the `flags` object, and the component’s flag handling is covered without any mocking ceremony. ## 8. Why use a first-party React SDK instead of rolling your own You can build a flag context yourself, and for a single boolean that never changes at runtime, you arguably should. The cost shows up as the app grows: per-flag subscriptions so one change does not re-render the page, a streaming connection with delta merging so live flips land, `useSyncExternalStore` so concurrent React does not tear, and StrictMode handling so development does not open duplicate connections. Each of those is a known footgun, and re-deriving them is the kind of undifferentiated work the [build-vs-buy decision](/blog/build-vs-buy-feature-flags/) is meant to catch. The pricing angle matters specifically for a client-side SDK. React flags evaluate in the browser, so on a usage-billed platform every user who loads the page is a metered event, and a successful launch that triples traffic also triples the flag bill. Featureflip uses [flat pricing](/blog/feature-flag-pricing-explained/) with unlimited evaluations on every plan, so client-side traffic does not move the invoice. The [pricing page](/pricing/) has the full numbers, and the [comparison pages](/compare/) break down how that lands against the per-MAU and per-seat vendors for a front-end-heavy app. ## The shorter version Feature flags in a React app are external state, and the idiomatic integration is small. Install `@featureflip/react` and `@featureflip/browser`, wrap your tree once in `` with a client key, and read any flag with `useFeatureFlag('key', defaultValue)`. The default value’s type picks the variation, so the same hook reads booleans, strings, numbers, and JSON config. Gate the flag-dependent subtree on `useFeatureflipStatus().isReady` so users never see the wrong variant flash, pass a `context` prop to target logged-in users and re-evaluate in real time, and use `TestFeatureflipProvider` to test every branch with no network. Under the hood the hook reads through `useSyncExternalStore` with a memoized snapshot, so only the component reading a changed flag re-renders, which is the part a hand-rolled context gets wrong first. ## Frequently asked questions ### How do I add a feature flag to a React app? Install `@featureflip/react` and `@featureflip/browser`, wrap your component tree in `` at the root, and call `useFeatureFlag('your-flag-key', false)` in any component inside the provider. The hook returns the current flag value and re-renders the component automatically when the value changes. That is the entire integration for a boolean gate: a provider at the top and one hook where you branch on the flag. ### What does useFeatureFlag return before the flag loads? It returns the default value you pass as the second argument. There is a short window between mount and the first flag load where every flag reads its default, so choose defaults that are safe to show briefly. To avoid rendering the default variant and then swapping, gate the flag-dependent subtree on `useFeatureflipStatus().isReady` and render a skeleton until the client reports ready, then render once with the loaded values. ### How does the React SDK avoid unnecessary re-renders? The hook subscribes through React’s `useSyncExternalStore` and memoizes a per-flag snapshot. A single shared context holding all flags re-renders every consumer whenever any flag changes, because the context value’s reference changes. The SDK instead compares each flag’s value (by identity for primitives, by serialized form for objects) and only notifies the components reading a flag whose value actually changed, so an unrelated flag update does not re-render your component. ### Do the hooks work with React Server Components? The hooks are client-side and run in Client Components, because they read live flag state that updates in the browser. In an app that uses Server Components, mark the components that call `useFeatureFlag` with `'use client'` and keep the `FeatureflipProvider` in the client tree. If you need to evaluate flags on the server during a render, use a server-side SDK in that code path instead of the React hooks. The SDK supports React 18 and 19 and works with the React Compiler and Suspense. ### Can I use feature flags in Next.js, Remix, or Vite with this SDK? Yes. The React SDK works in any React setup, including Next.js, Remix, Vite, and Create React App, because it builds on the framework-agnostic Browser SDK. The one framework-specific consideration is server rendering: in a server-rendered app, keep the provider and the flag-reading components in the client tree so the hooks run in the browser where flag state is live, and gate on `isReady` to keep the server-rendered markup and the first client render consistent. For the full server-rendering setup, including how to evaluate on the server to avoid the flash, see the companion guide on [adding feature flags to a Next.js app](/blog/feature-flags-in-nextjs/) . Featureflip is a focused, flat-priced feature flag platform with a first-party React SDK, sub-millisecond evaluation, and SSE streaming for live flag updates, so client-side traffic never meters the bill. The [React SDK reference](/docs/sdks/react/) has the full API, and you can [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-mcp-server/ # MCP Server for Managing Feature Flags in Claude Code [Blog](/blog/) # MCP Server for Managing Feature Flags in Claude Code The Featureflip MCP server lets Claude Code, Cursor, and other AI agents create flags, ramp rollouts, and clean up stale ones without leaving your editor. July 11, 2026 · Updated September 10, 2026 · - ai - mcp - automation You already let an AI assistant write the code behind a feature flag. It scaffolds the component, wires the SDK call, and drops in the `if (client.variation(...))` branch. Then the assistant stops at the editor boundary: it cannot create the flag, cannot turn it on in dev, cannot ramp it to 10% next week, and cannot tell you three months later that it is safe to delete. You alt-tab to the dashboard for all of that. Featureflip closes that gap, and it closes it on the free plan. The **MCP server** (`@featureflip/mcp`) gives AI coding assistants and autonomous agents a set of tools for managing your flags: create them, toggle them per environment, adjust targeting and rollouts, and find the ones ready for cleanup. Most flag platforms now ship an [MCP server](/glossary/model-context-protocol/) , so the interface itself has become ordinary. The interesting question is how far one reaches into a flag’s life. Featureflip’s works on the Free plan, so you can sign up, install it, and hand an agent real flags without a card or a sales call. This post covers how to add it to your editor in one line, the three workflows it is built for, and why it is safe to hand an agent a token. **Key Takeaways** - Agent-driven flag management runs on the **Free plan**, with no card and no sales call. Most flag platforms ship an MCP server now, so compare how far each one reaches into a flag’s life. - The **Featureflip MCP server** (`@featureflip/mcp`) lets AI assistants (Claude Code, Cursor, Copilot, Cline) and autonomous agents manage flags through the [Management API](/docs/management-api/overview/) , from inside the editor. - It runs on demand via `npx` with no service to deploy, and it authenticates with the **same bearer token** the Management API uses: a Personal Access Token (`ffp_`) for interactive editor use, or a Service Token (`ffs_`) for CI and unattended agents. - It manages flag **configuration, not evaluation.** Your app still reads flags through an [SDK](/docs/sdks/overview/) on the hot path. The MCP server is for creating, toggling, targeting, and cleaning up. - The tool set is deliberately **flag-focused and safe to automate:** the foot-guns (minting SDK keys, deleting projects or organizations) are left out, destructive tools are annotated so clients ask before running them, and every change is audit-logged and attributed to the token that made it. ## 1. A fourth way to reach your flags Featureflip has always had three front doors to the same flag configuration, each for a different caller. The **dashboard** is for a person making a change by hand. The **Management API** is for a script, a CI job, or an internal tool changing configuration programmatically. The **SDKs** are for your application reading a flag’s value for a user, fast, on the request path. The MCP server adds a fourth caller: the AI agent working alongside you. Model Context Protocol is an open standard for exposing tools to AI assistants, so any MCP-aware client, Claude Code, Cursor, VS Code Copilot, Cline, and others, can discover and call those tools directly. Under the hood the server is a thin translator. When your agent decides a flag should be created or toggled, it calls a tool; the server turns that into a Management API request and hands back the result. It is the automation surface you already have, reached the way an agent naturally works, one tool call at a time. That is the important framing: the MCP server does not add a new source of truth or a second place your flags live. It is the [Management API](/blog/feature-flag-management-api/) wearing an interface an AI assistant can use. Everything the agent does is a call you could have made yourself with `curl`. ## 2. Add it to your editor in one line There is nothing to install ahead of time and nothing to run as a background service. Your MCP client launches the server on demand with `npx`, so the only prerequisite is a token. Create one in the dashboard first. A **Personal Access Token** (prefixed `ffp_`, created under **Settings → API Tokens**) acts as you and is the right choice for interactive editor use, so the agent’s changes are attributed to you. A **Service Token** (prefixed `ffs_`, created under **Organization Settings → Service Tokens**) is a machine identity scoped to one organization, for CI or unattended agents. The token walkthrough is the same one the API uses, in the [authentication docs](/docs/management-api/authentication/) . In Claude Code, that is a single command: ```bash claude mcp add featureflip -e FEATUREFLIP_TOKEN=ffp_your_token -- npx -y @featureflip/mcp ``` Most other clients (Cursor, Cline, and anything that reads a standard MCP config file) take the same package as a JSON block: ```json { "mcpServers": { "featureflip": { "command": "npx", "args": ["-y", "@featureflip/mcp"], "env": { "FEATUREFLIP_TOKEN": "ffp_your_token" } } } } ``` Both forms run `npx -y @featureflip/mcp`. That is the whole setup. The [MCP server docs](/docs/integrations/mcp/) have the per-client details and the full tool reference. ## 3. Three workflows it is built for The tool set maps onto the life of a flag: introduce it, ramp it, retire it. Each stage is something you can now ask the agent to do in plain language, at the point in your work where it comes up. A flag's life expressed as agent tool calls. Wrapping creates it disabled everywhere; ramping widens exposure a step at a time; cleanup removes it once it is fully shipped. **Wrap new code in a flag.** Ask the agent to put a new feature behind a flag and it calls `wrap_feature`. That creates the flag (disabled in every environment, the way all new flags start) and returns an SDK snippet in your project’s language, which the agent applies to your working tree. When you want to try it, ask it to enable the flag in dev and it calls `toggle_flag` for that one environment. The flag stays off everywhere else until you say otherwise, so nothing reaches a user by accident. **Ramp a rollout.** For a flag that is already live, ask the agent to ramp it. It calls `update_targeting` with the rollout percentage set to 10, then 50, then 100 as your confidence grows, each call replacing the previous percentage rather than stacking on it. Because Featureflip uses [sticky bucketing](/glossary/sticky-bucketing/) , a user who lands in the rollout stays in it as the percentage climbs. Before a wholesale change, a well-behaved agent reads the current rules with `get_targeting` first, so it edits from the real state rather than guessing. **Clean up shipped flags.** Ask the agent to find cleanup candidates and it calls `find_stale_flags`, which returns flags that have been fully rolled out or left unchanged for a long stretch. [Flag cleanup](/blog/feature-flag-cleanup/) is the chore teams skip, and skipping it is how a codebase accumulates dead conditionals. For each candidate you confirm, the agent calls `delete_flag` and removes the matching branch from your code, leaving a single path where the flag used to be. A quick `flag_status` check first shows the flag’s state across every environment, so you confirm it really is finished before it goes. ## 4. It configures flags; it does not evaluate them The most useful thing to understand about the MCP server is what it deliberately leaves out. It does not evaluate flags. When your application needs to know what value a flag has for a given user, right now, on the request path, that is the [SDK’s](/docs/sdks/overview/) job, and the server SDKs answer it locally in memory in about the time of a map lookup, with no network round trip. The MCP server sits on the other side of that line, on the configuration plane. It creates the flag, sets its variations, decides which environments it is on, and writes the rules that determine who sees what. Those are low-frequency, deliberate changes. Keeping them off the evaluation path is the same separation the [Management API](/blog/feature-flag-management-api/) is built around, and it is why an agent hammering the configuration API can never slow down the part of your stack that has to stay fast. So the mental model stays clean. Use the MCP server (or the API, or the dashboard) to set a flag up. Use an SDK to read it out. The agent manages the switch; your code reads it. ## 5. Safe to hand an agent a token Giving an autonomous process write access to your flags is a reasonable thing to be cautious about. The server is designed around that caution in four ways. **A curated tool set, not the whole API.** The tools cover the flag lifecycle: projects, environments, flags, variations, targeting, and segment reads. The operations that would be genuinely dangerous in a loop are simply not exposed. There is no tool to mint or revoke SDK keys, no tool to delete a project or an organization, and segment writes stay in the dashboard. An agent cannot reach for a foot-gun that was never put on the table. **Destructive actions are labeled.** Every tool carries a standard MCP annotation marking it read-only or destructive. Listing and status tools are read-only and safe to call freely; `delete_flag`, `toggle_flag`, and `update_targeting` are marked destructive, which is the signal most clients use to pause and ask for your confirmation before running them. **Errors are written for a machine to recover from.** Featureflip’s API returns one frozen error shape, and the MCP server passes its hints straight through. A mistyped flag key comes back with a `did_you_mean` suggesting the closest match; a blocked action comes back with `next_actions` describing what to try instead. An agent reads those and self-corrects on the next call, instead of stalling and waiting for a human to decode a raw error. **Every change is on the record.** A flag toggled over MCP is audit-logged and attributed to the token that made it, exactly like a change made by hand in the dashboard or with `curl`. Requests are rate-limited per token. If you use a Service Token scoped to one organization, its reach is bounded to that organization no matter what the agent asks for. None of this is gated behind a sales call. The MCP server works on the Free plan, so letting an agent manage flags is available from the moment you sign up, not reserved for an enterprise tier. ## The shorter version Featureflip’s MCP server (`@featureflip/mcp`) gives AI assistants and agents a set of tools for managing feature flags from inside the editor, over the same Management API the dashboard and your scripts use. Add it to Claude Code or Cursor in one line with `npx`, authenticate with a Personal or Service token, and ask the agent to wrap new code in a flag, ramp a rollout from 10% to 100%, or find and delete flags that have shipped. It manages configuration, not evaluation, so your app still reads flags through an SDK on the hot path. The tool set is scoped to the flag lifecycle with the dangerous operations left out, destructive actions labeled so clients confirm them, errors written so an agent can self-correct, and every change audit-logged and attributed. It is available on the Free plan. ## Frequently asked questions ### Can AI agents manage my feature flags? Yes. The Featureflip MCP server (`@featureflip/mcp`) exposes feature-flag management as tools that any MCP-aware AI assistant, such as Claude Code, Cursor, VS Code Copilot, or Cline, can call. An agent can create a flag, enable it in an environment, adjust its targeting and rollout percentage, and find flags that are safe to delete, all from inside your editor. Under the hood it calls the [Management API](/docs/management-api/overview/) with a token you provide. ### What is the Featureflip MCP server? It is a small program that speaks the Model Context Protocol, an open standard for giving AI assistants tools. It runs on demand through `npx` with nothing to deploy, and it translates an agent’s tool calls into Management API requests. It is the same flag configuration surface as the dashboard and the REST API, reached the way an AI agent works. The [integration docs](/docs/integrations/mcp/) cover setup and the full tool list. ### Which AI tools does it work with? Any MCP client. That includes Claude Code, Cursor, VS Code with Copilot, and Cline, plus autonomous or CI agents that speak MCP. Interactive editors work best with a Personal Access Token (`ffp_`) so changes are attributed to you; unattended agents should use a Service Token (`ffs_`) scoped to one organization. ### Does the MCP server evaluate flags? No. It manages flag configuration: creating, toggling, targeting, and cleanup. Reading a flag’s value for a user at runtime is the job of the language [SDKs](/docs/sdks/overview/) , which evaluate locally in memory on your application’s request path. Keeping configuration and evaluation on separate surfaces is what keeps flag reads fast no matter how much an agent writes. ### Is it safe to let an agent change flags? It is built to be. The tool set is scoped to the flag lifecycle, with dangerous operations like minting SDK keys or deleting projects deliberately left out. Destructive tools are annotated so most clients ask for confirmation before running them, new flags start disabled in every environment, and every change is audit-logged and attributed to its token. A Service Token’s reach is bounded to a single organization. Featureflip is a focused, flat-priced feature flag platform, and the MCP server brings its flag management into the editor where you and your AI assistant already work: create flags, ramp rollouts, and clean up shipped ones over the same audited Management API, with a tool set scoped to be safe to automate. Read the [MCP server docs](/docs/integrations/mcp/) to wire it into your editor, see how the same configuration surface looks [declared as code in Terraform](/blog/feature-flags-as-code-terraform/) , compare plans on the [pricing page](/pricing/) , and [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-trunk-based-development/ # Trunk-Based Development with Feature Flags: What CI Misses [Blog](/blog/) # Trunk-Based Development with Feature Flags: What CI Misses Trunk-based development with feature flags moves risk from merge day to flag-flip day. A green trunk only proves the off path works. Here is the rest. August 28, 2026 · - trunk-based-development - ci-cd - best-practices Trunk-based development asks every developer to merge into one shared branch several times a day. DORA’s description of the capability is specific about batch size: [each developer divides their own work into small batches and merges that work into trunk at least once (and potentially several times) a day](https://dora.dev/capabilities/trunk-based-development/) , on branches that last hours instead of weeks. The obvious objection arrives immediately. Plenty of work takes longer than a day. The standard answer is a feature flag, and it is the right answer. Merge the unfinished code with the flag held off and nobody sees it until you decide they should. Most write-ups stop there. What they skip is that the flag did not delete the risk that used to sit in the merge. It relocated it, and the place it moved to has none of the ceremony that merge day always had. Worth noticing what DORA’s own page leaves out, too. It describes batch size and branch lifetime, and it never mentions feature flags at all. Flags are the mechanism teams reach for to hit that batch size. They are not part of the definition, and treating them as the whole answer is how the practice goes wrong. **Key Takeaways** - A flag stands in for the merge. Held off for three weeks, it is a long-lived branch with better version control. - “Trunk is always releasable” is a claim about one flag configuration, usually the one where every new flag is off. - A green pipeline proves the off path works. The on path, which is the code you are about to ship, needs its own run. - Two flag states carry almost all the value in CI: the one production runs now, and the one it runs a single flip later. - A flag hides code paths and cannot hide a schema change, which is why migrations need expand and contract underneath. ## 1. A flag replaces the merge, not the branch A long-lived branch defers two different things at once, and separating them makes the rest of this tractable. Integration is whether your code and everyone else’s have been reconciled. Release is whether a user can see the result. A branch defers both. Trunk-based development cancels the first deferral by making you merge constantly, and a [feature flag](/glossary/feature-flag/) supplies the second so you still choose when people see the work. That is the trade the [trunk-based development](/glossary/trunk-based-development/) glossary entry sets out in short form. So the flag takes over from the merge. Nothing takes over from the branch, because trunk-based development removed the need for one, and that is where the difference shows up later. Hold a flag off for three weeks and version control will tell you the code is integrated, because it is. Nothing runs it. No test exercises it in the configuration it will ship in, no colleague has hit it locally, and the strongest evidence available is that it compiled. Merge conflicts really are gone, which is worth having. The property that made long-lived branches dangerous in the first place, a large batch of unexercised change arriving in one go, is sitting on trunk untouched. Flag count is the wrong thing to watch. Track how long any single flag has sat in a state nothing exercises. ## 2. “Releasable” is a claim about one flag state Trunk-based development promises that trunk stays releasable. Ask which configuration and the promise gets narrower. Ten independent boolean flags describe 1,024 possible states of your application. Your pipeline runs one. Almost always it is the state an unconfigured environment produces, where every new flag falls back to its code default, and the code default is off because that is the safe thing to write. A green trunk therefore proves something real and small. The state production is already in still works. That is the state your users are in, so it matters. It says nothing about the state you are about to move to. Trunk-based development removes the reconciliation spike and leaves an exposure spike in its place. The second one is a dashboard toggle, which is why it gets treated as routine. That is the relocation. On a long-lived branch the risk piled up at the merge, where a fortnight of divergence had to be reconciled in one sitting, at a moment everyone already treated as dangerous. Flipping a flag changes which code path a million requests take. It happens through a toggle in a dashboard that gives no hint the same care applies. ## 3. Which flag states CI should actually run Every combination is an unreachable target. Twelve flags is 4,096 states, most of which cannot occur, several of which are nonsense, and every one of which somebody would have to maintain. Two states carry almost all the value. Start with the configuration production has right now. Your existing suite covers this already, assuming the test environment mirrors production defaults instead of drifting from them. Then run that same configuration with the single flag you are about to flip, flipped. Teams skip this second run, and it is the only one that speaks to the change actually being made. The mechanics are the same either way. Every Featureflip SDK ships a test client you build from a map of fixed values, so there is no mocking framework involved, and the per-language shape is written up in [testing code that uses feature flags](/questions/how-do-i-test-code-that-uses-feature-flags/) . The argument for covering both sides at all, with the Slack outage that makes it concrete, is rule four in the [feature flag best practices](/blog/feature-flag-best-practices/) pillar. What is specific to trunk-based development is where the second run lives. One CI job per open flag sounds thorough and rots fast, because nothing deletes the job when the flag goes. The convention that survives contact with a small team is duller: the pull request that introduces a flag introduces the on-path test in the same pull request, and that test dies with the flag because it sits in the same file as the code it covers. [Prerequisite flags](/blog/prerequisite-flags/) collapse the space when work is genuinely multi-part. Gate the sub-flags behind one parent and the group has a single off state, because a child cannot serve anything except its off variation while the parent is off. The evaluator applies that before any of the children’s targeting rules run, so it holds as a property of the configuration rather than a convention your code has to remember. ## 4. The one thing a flag cannot hide A flag gates a code path. A database migration is not a code path. By the time the flag decides anything the migration has already run, and it ran for everybody. This is where trunk-based development quietly breaks, because the flag creates a feeling of safety that the schema does not share. Merge the migration and the flagged read path together, ship on Tuesday, flip on Thursday, and the two days in between hold a schema that only unreleased code fully understands. The way out is expand and contract, which predates feature flags and pairs well with them. Expand first, additive changes only. A new column, a new table, a new nullable field. This deploys with no flag at all, because adding something nobody reads is safe. Then write both shapes, keeping the old one authoritative. Backfill the history so the two agree about the past as well as the present. Only now does the flag earn its place, and what it gates is the read path, meaning which shape this request trusts. Flip it, watch, and revert if it goes badly, because both shapes are still being maintained and rolling back costs a toggle. Contract last, and later than feels necessary. The old column goes after the flag is removed from the code, not after the flag is turned on. The flag gates the read path only. Everything additive ships unflagged, and the destructive step waits until the flag has left the codebase entirely. That ordering exists because the two halves have wildly different undo costs. Every step before the destructive one is undone by a flag flip, which takes seconds and needs no coordination. The destructive step is undone by a restore, which takes an incident and several people. Sequencing the work this way front-loads everything a toggle can still reverse. ## 5. Granularity, and when the removal clock starts One flag per releasable decision is the granularity that holds up. Per commit is too fine and produces flags nobody can name. Go the other way, one flag for a whole epic, and you lose the ability to release parts of the work separately, which was the point. A single flag stretched over six weeks of change is the long-lived branch again in a different hat. The useful test is whether you would ever want the two halves in different states in production. If yes, they are two flags. If no, one flag, and a [naming convention](/blog/feature-flag-naming-conventions/) is what keeps the resulting inventory legible to somebody reading it six months later. Removal is measured from the merge, and turning the flag on does not reset it. A flag that has been on for everyone for a month is finished work with a conditional wrapped around it, and the conditional is the part that costs you from then on. Pete Hodgson’s framing of toggles as [“inventory which comes with a carrying cost”](https://martinfowler.com/articles/feature-toggles.html) is the right model here, and trunk-based development raises the rate at which that inventory arrives. A team merging several times a day creates flags several times a week. Without a removal habit that becomes a rising floor of permanent complexity, and clearing out [dead feature flags](/blog/feature-flag-cleanup/) becomes a scheduled project rather than routine maintenance. ## 6. Where Featureflip fits Three things in the platform line up with the practice directly. Every SDK ships a test client, so running the on path takes a map of flag values and no mocking framework. Prerequisites let a multi-part feature present one switch to the outside world while staying several flags underneath. Staleness detection surfaces flags that have sat in one state long enough to look finished, which gives the removal habit something to run on besides memory. Pricing shape has a bearing on this too. Paid plans include unlimited flags at a flat monthly price, so a migration that needs eleven flags open for a fortnight bills the same as a quiet month. A platform that meters flags or evaluations puts a price on the exact behavior trunk-based development asks for, and teams respond to a meter by holding flags open longer and reusing them for work they were never created for. Read up on [the three pricing meters](/blog/feature-flag-pricing-explained/) before picking a platform, since whichever one you land on will shape how freely your team creates flags. ## The shorter version - Trunk-based development is about batch size and branch lifetime. Flags are the mechanism teams use to get there. - A flag substitutes for the merge, so a flag held off for weeks reproduces the long-lived branch inside trunk. - A green pipeline proves one flag configuration works, and it is usually the one production already runs. - Run two states: what production runs now, and what it runs one flip later. Add the on-path test in the same pull request that adds the flag. - Use expand and contract for schema work, and let the destructive step wait until the flag is out of the code. - One flag per releasable decision, with the removal clock starting at merge. ## Frequently asked questions ### Do you need feature flags for trunk-based development? Not strictly, and plenty of teams practice it without them by keeping every change small enough to finish inside a day. The moment a piece of work outgrows that, the choice is a branch that lives for a week or a flag that hides the incomplete path. Flags are how most teams keep the batch size down without blocking work that takes longer, which is why the two practices travel together even though DORA’s definition covers only the branching half. ### How long should a feature flag live in trunk-based development? Long enough to finish the work and validate it, and no longer. A release flag that is on for everyone with no plan to remove it has stopped being a flag and become a permanent conditional. Watch the age of the oldest flag sitting in a state nothing exercises, because that flag carries the same unexercised batch a long-lived branch would have carried. ### How do you test both paths of a feature flag in CI? Build a test client with fixed flag values and run your assertions against each side, which every Featureflip SDK supports without network access or mocking. The practical rule is to cover the configuration production runs today plus the configuration one flip away, rather than attempting every combination. Adding the on-path test in the same pull request as the flag keeps the coverage tied to the flag’s lifetime, so it disappears when the flag does. ### How do database migrations work with trunk-based development? Through expand and contract, because a flag gates code paths and cannot gate a migration that has already run. Ship the additive change unflagged, write both shapes while the old one stays authoritative, backfill, and put the flag on the read path only. Drop the old shape after the flag has been removed from the code, since every earlier step can be reversed with a toggle and that one can only be reversed from a backup. ### Is trunk-based development the same as continuous integration? They are tightly linked and not identical. Trunk-based development is the branching model, meaning small frequent commits to one shared branch in place of long-lived feature branches. Continuous integration is the practice of building and testing every one of those commits automatically. Trunk-based development is what makes continuous integration meaningful, because integrating constantly is only useful if something verifies the result each time. Featureflip is a flat-priced feature flag platform with a test client in every SDK, prerequisite flags for multi-part work, and staleness detection so finished flags do not quietly become permanent. Paid plans carry unlimited flags, so the bill does not climb as your team creates more of them. [Start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/feature-flags-vs-environment-variables/ # Feature Flags vs Environment Variables: A Practical Guide [Blog](/blog/) # Feature Flags vs Environment Variables: A Practical Guide Env vars configure where the process runs; flags decide what each request sees. Decision rule, comparison table, and 4 production gotchas inside. April 25, 2026 · Updated July 30, 2026 · - config - fundamentals Environment variables and feature flags both control application behavior, but they solve different problems. Conflating them leads to fragile systems, awkward deployment workflows, and flags that outlive their usefulness by years. The doctrine of putting config in the environment goes back to [The Twelve-Factor App](https://12factor.net/config) ; the concept of a feature toggle was formalized by [Martin Fowler and Pete Hodgson](https://martinfowler.com/articles/feature-toggles.html) , who split toggles into release, experiment, ops, and permission categories. This post lays out when each tool belongs, and where teams confuse them. **Key Takeaways** - **Env vars** configure *where* and *how* a process runs (database URLs, API keys, log levels). They are static for the life of the process and require a redeploy to change. - **Feature flags** configure *what a specific request or user sees*. They are evaluated per-request, support targeting, and change without a deploy. - The one-line rule: if two users hitting the same running instance could ever need different values, it’s a flag. If not, it’s an env var. - The most common mistake is `ENABLE_X=true` env vars repurposed as poor-man’s flags. Fine until the day you need a 10% rollout or a 2 AM kill switch ([feature flag cleanup playbook](/blog/feature-flag-cleanup/) ). ## What each one is **An environment variable** is a named string value set in the process environment at startup and treated as static for the lifetime of that process. It configures where the application connects, what mode it runs in, and what secrets it uses: things that differ between deployment targets, not between users. **A feature flag** is a named boolean or multi-variant value evaluated at runtime, typically over a remote source of truth, that controls which code path executes for a given request or user. It changes behavior without a redeploy, and can be scoped to a specific segment of traffic. ## When should you use environment variables? Environment variables excel at configuration that is static within an environment, secret, or determined before the process starts. The Twelve-Factor App’s third factor codifies this: anything that varies between deploys (credentials, hosts, per-environment toggles) should live in the process environment, not in source code ([12factor.net](https://12factor.net/config) ). Env vars are read once at startup and treated as immutable for the process’s lifetime. ### 1. Database connection strings `DATABASE_URL` is the canonical example. It points to your Postgres instance, includes credentials, and is different in dev, staging, and production. It never changes while the app is running. Storing it as an env var means it stays out of source code, can be rotated by updating the deployment secret, and doesn’t require any runtime evaluation logic. ### 2. API keys and secrets Third-party service keys (payment processor secrets, object storage credentials, outbound email API keys) are secrets first and configuration second. Env vars compose naturally with secret management systems (Kubernetes secrets, Vault, Doppler) and satisfy security policies that require secrets to be kept out of application code. Evaluating a feature flag to find a secret is the wrong abstraction entirely. ### 3. Third-party service endpoints `STRIPE_API_BASE`, `OPENAI_API_HOST`, `S3_ENDPOINT`. These are environment-level choices. Staging points at sandbox endpoints, production points at live ones. These don’t change per-user and don’t need runtime toggle semantics. ### 4. Build-time and runtime mode flags `NODE_ENV=production`, `RAILS_ENV=production`, `ASPNETCORE_ENVIRONMENT=Production`. These inform the framework itself, not just your code. They affect which config files load, whether debug middleware is enabled, and how assets are bundled. They must be set before the process starts and cannot meaningfully be changed mid-flight. ### 5. Log levels and observability config `LOG_LEVEL=warn`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `SENTRY_DSN`. These affect how the process reports on itself. They are environment-wide, they configure external systems, and they often come from your platform’s secrets store. Routing them through a feature flag system adds a circular dependency (what if the flag system itself fails before logging is configured?). **The common thread:** env vars are for *where* and *how* the process runs, not *what* it does for a specific user. ## When should you use feature flags? Feature flags shine when the question is: “should this code path execute, for whom, and starting when?” Martin Fowler’s canonical taxonomy splits flags into four categories (*release*, *experiment*, *ops*, and *permission* toggles), each with different lifetimes and ownership ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Three of those four are impossible to express cleanly as env vars, because they need per-request, per-user, or runtime-mutable evaluation. ### 1. Gradual rollouts You’ve merged a rework of your checkout flow. You want to expose it to 5% of users, monitor error rates and conversion, then ramp to 100% if the metrics look healthy, all without a second deployment. A feature flag does this; an env var does not. ### 2. Kill switches Certain features carry operational risk: a new third-party integration, a resource-intensive background job, a new payment provider. A kill switch flag lets an on-call engineer disable it in seconds without touching infrastructure. An env var change requires a process restart (typically a rolling deploy that takes minutes, not seconds) and introduces its own risk. ### 3. A/B tests and experiments Testing two button labels, two pricing page layouts, or two recommendation algorithms requires serving different variants to different users within the same deployed build. That’s a flag, specifically a multivariate flag, not a config value. ### 4. Per-user and per-segment targeting Beta programs, internal dogfooding, enterprise tenant overrides, and geographic feature launches all require the same question: “should user X get behavior Y?” Env vars have no concept of a user context. Flags evaluate against user attributes, segment membership, or a deterministic hash of the user ID. ### 5. Paywall and plan-based variations Showing premium features to paid users, gating beta features behind an opt-in, or launching a new UI only for enterprise accounts: these are targeting decisions made at evaluation time, per-request. Flags model this directly. A build-time config cannot. ### 6. Dark launches You want to call the new code path in production, observe its behavior, and collect metrics, but not yet show its output to users. Wrap it in a flag that’s off for everyone, deploy, then turn it on for internal users only. This is impossible to express cleanly as an env var. **The common thread:** flags are for *what a specific request or user experiences*, and for control you need to exert without touching deployment. ## Side-by-side comparison | | Environment variable | Feature flag | | --- | --- | --- | | Changes at runtime | No, requires process restart or redeploy | Yes, evaluated on every request | | Per-user targeting | No, process-wide value | Yes, evaluates against user context | | Audit log | No built-in history | Yes, changes tracked with timestamp and actor | | Restart required | Yes, set at process startup | No, changes propagate to running instances | | Granularity | Environment (dev / staging / prod) | Per-user, per-segment, percentage-based | | Typical lifetime | Indefinite (rotated when credentials change) | Weeks to months (should be cleaned up after rollout) | The table understates one dimension: **change-propagation latency**. The order-of-magnitude gap between deploying a new env var and flipping a flag is what makes flags suitable for kill switches and incident response. Log scale. Flag SDKs propagate via streaming or short polling intervals; env var changes need a process restart (typically a rolling deploy); pure code changes also pay a CI build. That 100×–1000× gap is why kill switches and gradual rollouts belong in a flag system. ## Decision flowchart: which one do I reach for? If you'd ever want two users in the same running instance to see different values, it's a flag. Otherwise it's an env var. If you prefer it as text: - **Secret, credential, or static per deployment** (API keys, `DATABASE_URL`, `LOG_LEVEL`) → environment variable. - **Runtime change, per-user targeting, or temporary rollout** (kill switches, A/B tests, beta gates) → feature flag. - **Still ambiguous?** Ask whether two users hitting the same running instance could ever need different values. If yes → flag. If no → env var. ## Common pitfalls ### Using env vars as poor-man’s feature flags The most frequent mistake: an engineer creates `ENABLE_NEW_CHECKOUT=true` in the environment to toggle a feature. This works until you need to roll it out to 10% of users, or turn it off at 2 AM without waking up the DevOps rotation. At that point the team discovers they’ve built a deployment-gated toggle instead of a runtime one. Changing it requires a redeployment and process restart, not a flag flip, and migrating the semantics while the feature is already in production is uncomfortable. The kill-switch case has real-world stakes: the [Knight Capital incident](/blog/feature-flag-cleanup/) — $460M lost in 45 minutes when a deprecated flag’s bit was reused — is the textbook example of a toggle that should have been a runtime kill switch backed by a cleanup workflow. ### Using flags for things that never change The inverse mistake is routing static infrastructure config through a flag system. `POSTGRES_MAX_CONNECTIONS` or `REDIS_CLUSTER_HOST` do not need audit logs or gradual rollout semantics. Adding them to a flag system increases the surface area for misconfiguration and creates a dependency on the flag service during startup, which is exactly when you want the fewest external dependencies. ### Stale flags rotting in the codebase Feature flags are temporary by design, but in practice they rot. Most are never removed: Pete Hodgson frames toggles as “inventory which comes with a carrying cost” that teams have to actively keep low ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). A flag for a rollout that completed eight months ago is dead code wrapped in an `if` statement, and nobody is sure whether it’s safe to remove. Build retirement into your workflow: when a flag hits 100% rollout and metrics are stable, schedule the cleanup. The [four-step cleanup playbook](/blog/feature-flag-cleanup/) (detect, triage, remove, prevent) covers the specific procedure. ### Leaking configuration concerns across layers Checking `process.env.ENABLE_NEW_CHECKOUT` deep in a service module creates an implicit coupling between deployment config and business logic. Flag evaluation, by contrast, passes a user context explicitly. This makes the behavior testable (inject a flag client that returns the value you want), auditable (the flag system records who changed it), and decoupled from deployment. ## What flag evaluation looks like in production code Here’s a concrete example using Featureflip’s Node.js SDK: ```ts import { FeatureflipClient } from "@featureflip/node"; const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, }); const currentUser = { id: "user-123" }; // from your auth layer const checkoutV2Enabled = client.boolVariation( "checkout-v2", { user_id: currentUser.id }, false, // default if evaluation fails or flag is missing ); if (checkoutV2Enabled) { // new flow } else { // old flow } ``` The SDK key itself is an env var: it’s a credential scoped to an environment. The flag evaluation is runtime, per-user, and falls back gracefully to `false` if the flag service is unreachable. The SDK key never changes between requests; the flag result does. For more on how Featureflip models targeting, segments, and rollout percentages, see the [rollout strategies](/docs/concepts/rollout-strategies/) and [environments](/docs/concepts/environments/) docs. ## Frequently asked questions ### Can I use environment variables for A/B testing? Not effectively. A/B testing requires serving different variants to different users within the same running process, based on a stable hash of the user ID or a targeting rule. Environment variables are process-wide (every request sees the same value), so you can’t split traffic without running multiple deployments. Use a multivariate feature flag instead. ### How long should a feature flag live in the codebase? Most rollout flags should be retired within weeks of hitting 100%. Permanent kill switches and entitlement flags (paywall, plan tier) live indefinitely by design. The trap is rollout flags that quietly become permanent, and most flags are never removed in practice — toggles are “inventory which comes with a carrying cost” ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). Treat retirement as part of the rollout, not an afterthought. ### Are feature flags a security risk? They can be, if misused. Putting secrets in a flag system is the obvious mistake: flag values are typically cached on the client and visible to anyone who inspects the SDK payload. Use environment variables and a secrets manager for credentials. Flags are safe for behavior toggles and targeting rules, which don’t expose sensitive data even if the flag config leaks. ### What happens if the feature flag service is down? Every reputable SDK is built to degrade safely: evaluations fall back to the default value passed in code, and the SDK retries the connection in the background. That’s why the third argument to `boolVariation(...)` is `false` in the example above. It’s the value served if the flag service can’t be reached, so your app stays up and the new code path simply doesn’t activate. ### Should I version-control my feature flag definitions? You can, and on Featureflip that’s what the [Terraform provider](/docs/integrations/terraform/) is for. Projects, environments, flags and their variations, segments, and per-environment targeting are all declarable in `.tf` files, so flag structure gets the same review and history as the rest of your infrastructure. Day-to-day toggling is the exception. Turning a flag off during an incident is a dashboard or API action, and a `terraform apply` is the slower path to it. Targeting needs care for a different reason. Once you set `rules` on a flag-environment, Terraform owns every rule there, and anything added in the dashboard disappears on the next apply. Without the provider, keep three things in source control: the flag *key* used in code, the default value passed to the SDK, and a comment explaining what the flag gates and when it should be retired. ## The one-line rule Configuration that belongs to the **process** goes in env vars. Configuration that belongs to the **request or user** goes in flags. Env vars answer “where does this service connect?”; flags answer “what does this user see?” If you remember one heuristic from this post: ask whether you’d ever want this value to differ between two users hitting the same running instance. If yes, it’s a flag. If no, it’s an env var. For a deeper reference on how Featureflip models flags, targeting rules, and evaluation context, see [Feature flags overview](/docs/concepts/feature-flags/) . For the failure modes that come from misusing flags once you have them, see [feature flag anti-patterns](/blog/feature-flag-anti-patterns/) . And for the broader picture — the eight rules production teams converge on for working with flags — see [the full feature flag best-practices pillar](/blog/feature-flag-best-practices/) . --- ## Source: https://featureflip.io/blog/gitops-feature-flags/ # GitOps for Feature Flags: How the Workflow Actually Runs [Blog](/blog/) # GitOps for Feature Flags: How the Workflow Actually Runs Run feature flag config through git: where it lives, what a reviewer checks, how CI applies it, and why some configuration drift is correct and should stay. August 1, 2026 · - gitops - automation - best-practices GitOps for feature flags means the flag configuration that outlives any single release lives in a git repository, changes through pull requests, and reaches the platform through CI. Which flags exist, what variations they carry, which segments they target, what the targeting rules say. All of it declared, diffed, reviewed, and applied by a machine. The practice is [flags as code](/glossary/flags-as-code/) , and this is the workflow around it. There is one wrinkle, and it is the reason this workflow needs its own treatment rather than a copy of the one you already run for infrastructure. Feature flags are the single piece of production you deliberately change outside the pipeline. A kill switch that waits on a plan and an apply is a kill switch that failed. So the interesting question is where the declared path stops and the fast path begins, and what happens to the gap between them. **Key Takeaways** - Declare the structure, which is flags, variations, segments, and stable targeting. Leave incident-time toggling on the fast path. - The highest-blast-radius line in a flag diff is usually the fallthrough variation, and it looks like nothing. - Rule order carries meaning. A diff that only moves a block changes who gets what. - Some drift is correct. A flag flipped at 3am is drift, and reverting it automatically would be the bug. - Run plan on the pull request with a read-only token, and apply on merge with a scoped write token. ## 1. What GitOps means when the thing you declare is a flag GitOps is a narrow idea with a big name. Desired state lives in a repository, something reads it, and that something makes reality match. Most people met the pattern through Kubernetes, but nothing in it is specific to clusters. The part that transfers cleanly to flags is the structural configuration. A flag’s existence, its type, its variations, the segments it targets, and the shape of its rollout rules are all long-lived and they accumulate. Six months in, nobody remembers why the flag that gates checkout targets three overlapping segments, and no dashboard will tell you. A repository will. The part that does not transfer is the reconciliation loop. Infrastructure tooling assumes any difference between declared and actual state is a defect, so it corrects on a schedule. Flags break that assumption on purpose. Section 5 covers what to do about it, because getting this wrong is the most expensive mistake in the whole workflow. Flag **configuration** is the whole scope here. Reading a value for a user at runtime stays in your application through an [SDK](/docs/sdks/overview/) , in memory, on the request path, and nothing described below touches it. ## 2. Where the configuration lives Two layouts work, and the choice is about who reviews changes rather than about tooling. **Co-located with the service.** Flag declarations sit in the same repository as the code that reads them, usually beside whatever infrastructure that service already declares. Reviewers are the people who own the feature, so they know what the flag is for. This is the right default while your flag count is small and each flag belongs to one team. **A dedicated flags repository.** All declarations in one place, reviewed by whoever owns release process. This starts to pay off once several teams share segments, or once someone needs to answer “what is enabled in production right now” without cloning six repositories. Either way, the tool writes state, and that state holds secrets. SDK keys in particular are readable in plaintext once they land there, so the state backend needs the same treatment as any other secret store. The [Terraform provider guide](/blog/feature-flags-as-code-terraform/) covers the specifics of that, along with the resource types and the import path for flags you already created by hand. ## 3. What a reviewer is actually looking at This is where a flag pull request stops resembling an application pull request, and it is the part most teams learn by getting it wrong once. Start with the fallthrough. One line, reads like a default, decides what happens to every user who matches no rule: ```diff resource "featureflip_flag_environment" "checkout_prod" { environment = "production" enabled = true default_variation = "false" default_variation = "true" } ``` That diff turns the feature on for everyone the targeting rules did not already catch, which is a full release expressed as a one-word change, sitting under two lines that did not change, in a file where the eye is drawn to the rules block below it. Reviewers who scan for large diffs miss it every time. Then check rule order. Rules evaluate top to bottom and the first match wins, so list position is priority, and a diff that moves a block without editing a character of it still changes who gets what. Git renders that as a delete and an insert somewhere else in the file, so it reads as churn when what actually changed was the targeting. After that, three quicker passes: - **Which environment is this?** A `production` resource and a `staging` resource differ by one string and are otherwise identical. Search the diff for the environment name before reading anything else. - **Did a percentage move, and does the bucketing hold?** Ramping 10 to 25 is fine when bucketing is [sticky](/glossary/sticky-bucketing/) , because the original 10% stay in. Changing the attribute the bucketing keys on reshuffles everyone, which is a different change wearing the same clothes. - **Is a segment definition changing under a flag that references it?** Editing a segment is a targeting change to every flag that uses it, and none of those flags appear in the diff. None of this requires the reviewer to be the person who wrote the feature. It does require a review checklist, because the failure mode here is a diff that approves itself on the strength of being small. ## 4. Plan on the pull request, apply on merge The pipeline is two jobs and one asymmetry between them. On pull request, run the plan and post its output as a comment. The HCL diff shows intent and the plan shows consequence, including every resource the change touches that nobody thought about, so the plan is what the reviewer should be reading. For flag work those two diverge more often than they do for infrastructure, because a single segment edit fans out. On merge to your default branch, apply. Authenticate with a service token scoped to one organization, with a project allowlist, rather than a personal token that carries whatever access its owner happens to have. The [Management API authentication docs](/docs/management-api/authentication/) cover the token types. Give the plan job a read-only token and the apply job a write token. Plan runs against pull requests, which means it runs against code from forks and from anyone who can open one. Read-only there costs nothing and closes the obvious hole. Every change now also carries a commit message, an author, a review, and a history with no retention limit. Featureflip records [every flag change automatically](/docs/guides/audit-log/) with actor and before-and-after values, and that log is the right tool during an incident, but it ages out at 7 days on the free plan. Git keeps the structural history for as long as the repository exists. ## 5. Drift, and why some of it is correct Both paths write to the same platform, at very different speeds. Both paths write to the same platform. The gap they open is drift, and closing it is a human decision. Drift is any difference between what the repository declares and what the platform is actually serving. In infrastructure that difference is a defect. Here it is often the system working exactly as designed, because someone used the [kill switch](/use-cases/kill-switch/) at 3am and saved the evening. Two rules cover it. **Declare only what you are willing to have reverted.** Anything under the tool’s control goes back to the declared value on the next apply, without ceremony. That is the correct behavior for a segment definition and the wrong behavior for the on/off state of a flag your team ramps daily. Most teams land on managing existence, variations, and segments everywhere, then managing rules only for the flags whose targeting is genuinely stable. **Reconcile on a decision, not on a timer.** Scheduled auto-apply is a normal, sensible pattern for infrastructure and a foot-gun here, because it will silently re-enable a feature somebody turned off for a reason at some point when nobody is watching. Apply on merge. A human is always in the loop, which is the property you want. Real drift then has an obvious home, and it makes the paperwork do double duty. After the incident, open a pull request that brings the repository in line with what the dashboard now says. The diff records what changed, the description carries the reason, and the review is where somebody finally asks whether it should stay that way. One artifact, and you have both the reconciliation and the incident write-up. ## 6. Promoting a change through environments Environments are separate resources, so a change reaches staging and production as separate declarations. That is verbose, and the verbosity is doing real work, since the whole reason [environments](/docs/concepts/environments/) exist is that they hold different values. The failure here is copy-paste. Someone gets targeting right in staging, copies the block, changes the environment string, and ships a production rule referencing a segment that only exists in staging, or a percentage that made sense against staging traffic. Copy the structure deliberately and re-derive the numbers. Promotion itself is a normal pull request. The reviewer’s job is the section 3 checklist with more attention, because the environment string is the only thing separating the two. ## 7. What this costs Being straight about the trade, since the tooling pages rarely are. Every structural change is now slower. That is the point, and it is still a cost, and it lands hardest on the person who wanted one flag for one afternoon’s experiment. Give people an escape hatch for genuinely temporary flags or they will route around the whole thing. Your state file is now a secret, with real consequences if it leaks. Your team needs to know that rules are authoritative once declared, or somebody’s dashboard edit vanishes on a Tuesday and trust in the pipeline goes with it. And below a handful of flags managed by one team, the honest answer is that this is overhead with no return. The workflow starts paying when nobody can hold the flag inventory in their head, which is also roughly when [flag cleanup](/blog/feature-flag-cleanup/) stops happening by itself. The pattern still earns its place. Adopt it in the order the value arrives, starting with flag existence and segments, adding targeting once your rules stop moving weekly, and leaving toggling out of it permanently. ## The shorter version GitOps for feature flags puts the structural configuration into a repository and applies it through CI, so flag changes get the review, history, and reproducibility that the rest of your infrastructure already has. Declare flags, variations, segments, and stable targeting. Review the fallthrough line first, then rule order, then which environment the diff touches. Plan on the pull request with a read-only token, apply on merge with a scoped write token. Leave incident-time toggling on the fast path, treat the drift it creates as correct, and fold it back into the repository afterwards as a pull request that doubles as the incident record. Adopt it when the flag inventory outgrows anybody’s memory, rather than on day one. ## Frequently asked questions ### What is GitOps for feature flags? GitOps for feature flags is managing flag configuration as declared state in a git repository, reviewed through pull requests and applied to the flag platform by CI. The declared state covers structure: which flags exist, their variations, the segments they target, and their targeting rules. It deliberately excludes runtime evaluation, which stays in your application through an SDK, and usually excludes day-to-day toggling, which needs to be faster than a pipeline allows. ### Should feature flag targeting rules live in git? For flags whose targeting is stable, yes, because a rule change is a production behavior change and it benefits from a diff and a reviewer. For flags your team iterates on daily, leave the rules out and manage only the flag’s existence and variations. Declared rules are authoritative, and that is what decides it. Once a tool manages them, a rule added in the dashboard is removed on the next apply, which is correct for stable targeting and disruptive for a flag somebody is actively tuning. ### How do you handle a flag that was toggled during an incident? Leave it. The toggle was the right call and reverting it automatically would undo an incident response. Afterwards, open a pull request that updates the repository to match the platform, with the reason in the description. That closes the drift, creates a reviewable moment to decide whether the change should stay, and leaves a permanent record of what happened. Avoid scheduled auto-apply on flag configuration, since it turns this from a decision into a surprise. ### Do you need Terraform to manage feature flags as code? No. Any tool that can drive the platform’s REST API works, including a script in CI that reads a YAML file and calls the [Management API](/blog/feature-flag-management-api/) . Terraform is the common choice because it brings state, drift detection, and a plan step for free, and because most teams already run it. The requirement is that configuration lives in version control and reaches the platform through review. Which tool performs the last step is secondary. ### Does managing flags in git slow down releases? It slows down flag configuration changes. Releases are unaffected. Creating a flag or editing a segment now takes a pull request. Releasing behind a flag that already exists is unaffected, and turning a flag off during an incident is unaffected, because both stay on the fast path. Teams that report the workflow slowing them down have usually put toggling under version control too, which is the one thing to keep out of it. Featureflip is a flat-priced feature flag platform, and flags-as-code is on every plan rather than reserved for an enterprise tier. The [Terraform provider](/docs/integrations/terraform/) and the [public Management API](/blog/feature-flag-management-api/) both work on the free tier, alongside the [MCP server](/blog/feature-flags-mcp-server/) if you would rather drive flags from your editor. See the [step-by-step Terraform guide](/blog/feature-flags-as-code-terraform/) for the resource reference and the import path, or [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/new-plans-sso-flag-lifecycle/ # Feature Flag Owners, Expiry Dates, SSO and New Plans [Blog](/blog/) # Feature Flag Owners, Expiry Dates, SSO and New Plans Free now covers 3 people and Team is new at $49. Flags get expiry dates and named owners, someone hears when a flag is done, and Business adds SSO. September 30, 2026 · - release - cleanup - ownership - pricing - sso Nobody creates a feature flag meaning to keep it forever. It goes in for a rollout, and everyone in the room agrees it comes out when the rollout is done. Then the rollout finishes, whoever made the flag moves on to the next project, and the flag sits in the code for a year. Nobody’s name is on it. Nothing ever asks about it. Tools have been able to spot a dead flag for years. Spotting it puts no date on anyone’s calendar and no name in anyone’s inbox, and in practice those two things are what get a flag deleted. That’s most of this release. Any flag can now carry an expiry date, on every plan. When a flag passes its date, or has done nothing for 90 days, Team and above get an email about it, and on Pro and Business it goes to the flag’s named owner. The plans changed too. Free now covers three people, Team is a new tier at $49 a month, and Business adds single sign-on through your own identity provider. Outbound webhooks, which shipped a few days earlier, round it out. **Key Takeaways** - Every plan, Free included, can set an expiry date on a flag. Passing it marks the flag stale and leaves what the flag serves untouched. - On Team and above, a flag that expires or goes dead triggers a cleanup notice: to the flag’s owner on Pro and Business, and to your Owners and Admins on Team. - Pro and Business name an owner for each flag. When someone leaves, the admin removing them picks who inherits their flags. - Free covers up to 3 people. Team is $49 a month, Pro $99 and Business $249, and Business adds SSO through Okta, Microsoft Entra ID or any OpenID Connect provider. - Paying for Pro or Business today? Your price holds until you change plan or switch between monthly and annual billing. ## 1. Why flags outlive the work they were for Pete Hodgson’s long write-up on feature toggles calls them “inventory which comes with a carrying cost” ([martinfowler.com](https://martinfowler.com/articles/feature-toggles.html) ). You pay that cost in small amounts, constantly. Every flag is a branch someone reads around, a second code path your tests may or may not reach, and one more thing to rule out at 2 a.m. when production misbehaves. Is this one still live? Who would know? Featureflip already sorts flags into Active, Stale and Dead from real evaluation traffic, and the [stale flag detection](/docs/concepts/stale-flags/) docs explain the signals. A badge in a dashboard only helps whoever opens the flags list and goes looking, and by then the person best placed to act on it has often left the team. So two pieces were missing. A flag had no written end, which meant “finished” was always inferred from traffic after the fact. And a flag had no person attached, so a stale signal had nowhere to go. The next three sections are about closing those two gaps. What changes this week is the middle of that line. A flag used to go from "rolled out" straight to a badge nobody opened. Now its date or its traffic sends a named person an email, and the cleanup Action can take it from there. ## 2. Give a flag an end date the day you create it The Create Flag page has a new optional **Expires on** field, and every existing flag has a **Set expiry** control in its header. Pick the day you expect the flag to come out. Once the date passes, the flag is marked Stale with the reason `PastExpiry`, and the flags list labels it Expired. Evaluation is unaffected. A flag serving `true` to half your users keeps doing exactly that the morning after, because the date is a note to your team and the evaluation engine ignores it. If the rollout slipped, move the date, or clear it. Both remove the stale marking straight away. A flag has to look finished for 30 days before the traffic signals call it Stale, and 90 before they call it Dead, so a quiet week in staging doesn’t read as abandonment. An expiry date skips the wait entirely, since you wrote the date down yourself and there’s no ambiguity to sit out. The [flag cleanup Action](/docs/integrations/flag-cleanup-action/) sees the date too, but it won’t open a removal pull request on a passed date alone, because a typo in a date should never make a live flag look safe to delete. It waits until traffic agrees. You can also set dates through the Management API or the MCP server, so an AI agent creating a flag for you can give it an end in the same call. The [flag expiration guide](/docs/guides/flag-expiration/) covers every route. ## 3. Cleanup notices land with someone, once Every day Featureflip looks for flags that have passed their expiry date or been dead for 90 days, and emails a digest to whoever should act. The subject line is blunt: “Feature flag checkout-v2 is ready to remove”, or “3 feature flags are ready to remove” when several arrive together. Inside, flags are grouped by project and each one links to its page in the dashboard. A flag gets one notice and then goes quiet, so you won’t get a daily reminder about a kill switch you’ve decided to keep. If it comes back to life and later goes dead again, you’ll hear about it again, with at least 30 days between notices for the same flag, which stops a flag flickering at the edge of the traffic signal from turning into a stream of email. Who receives it depends on the plan. On Pro and Business the notice goes to the flag’s owner. Unowned flags, and every flag on Team, go to the organization’s Owners and Admins, under a heading that explains why they’re getting it. Anyone can switch these off. It’s the **Flag cleanup notices** toggle under Profile, then Email Preferences, or the unsubscribe link at the foot of any notice. The setting is per person, so one admin opting out doesn’t silence everyone else. Notices are a Team feature. On Free the same flags still get caught: each expired or dead one is marked in the flags list, and the cleanup Action opens the removal pull request once a flag goes dead. Team adds the email, so somebody hears about a finished flag without opening the dashboard. ## 4. Flag owners put a name next to every flag On Pro and Business, every flag can have an owner, set from the flag’s header, and the flags list shows who owns what. The owner is the person who can answer the question every cleanup eventually hits. Was this a finished rollout, or a kill switch someone meant to keep? Remove a member who owns flags and the dialog tells you how many they own and asks who should inherit them. Pick a teammate and the flags move in the same step. Or leave them unowned, and their notices go to your Owners and Admins until someone claims them. On Free and Team, Featureflip still quietly records who created each flag. Nothing is routed by it. It means that when you upgrade to Pro, your flags already have owners (the people who made them), and nobody has to reconstruct who built what two years ago from `git blame`. ## 5. Webhooks tell your other systems Outbound webhooks went out a few days before the rest of this release. Choose the events you care about, from a flag toggling to a targeting rule changing to an SDK key being rotated, and Featureflip sends a signed HTTP request to your endpoint the moment it happens. There are 29 event types, and a subscription can be narrowed to particular projects and environments, so a staging pipeline never hears about a production toggle. Posting production toggles into the channel your on-call engineers watch is the obvious first use. Others are triggering a deploy, refreshing a cache, or feeding every change into your own audit pipeline. Each delivery is signed, retried if your endpoint fails, and kept in a delivery log you can resend from once the endpoint is back. You can manage subscriptions in Organization Settings, the Management API, the MCP server or the [Terraform provider](/docs/integrations/terraform/) . Free includes one endpoint, Team and Pro three, and Business as many as you need. The [webhooks guide](/docs/guides/webhooks/) has payloads and signature verification. ## 6. Single sign-on through your own identity provider Business now connects to the identity provider your company already runs. Featureflip uses OpenID Connect, so Okta, Microsoft Entra ID and any provider that publishes an OIDC discovery document will work. The [Okta guide](/docs/guides/sso-okta/) and the [Entra ID guide](/docs/guides/sso-entra-id/) each include a Terraform config for the IdP side, if you’d rather not click through an admin console. Verify your email domain with a DNS TXT record, register Featureflip in the IdP, then save, test and activate the connection. After that, people on your domain sign in with **Continue with SSO**. With **Add new people automatically** switched on, anyone your IdP lets in joins with a default role you choose, Viewer, Member or Admin, and giving a new engineer access becomes a group assignment in your IdP. **Require single sign-on** makes your IdP the only door into the organization, and Owners keep a password recovery path so a misconfigured IdP can never lock everyone out. The [single sign-on guide](/docs/guides/single-sign-on/) walks through all of it. Google and GitHub sign-in remain on every plan. They’re a convenience for individual accounts, where SSO is your company’s IdP deciding who gets in. ## 7. The new plans Four self-serve plans now, each a flat monthly price. No metered seats, monthly active users or evaluations, and no overage. | Plan | Price | Team members | Projects | Environments per project | What it adds | | --- | --- | --- | --- | --- | --- | | Free | $0 | Up to 3 | 1 | 2 | 10 flags per project, 1 webhook endpoint, 7-day audit log | | Team | $49/mo ($39 annual) | Up to 5 | 3 | 3 | Unlimited flags, scheduled changes, 3 webhook endpoints, 30-day audit log | | Pro | $99/mo ($79 annual) | Up to 10 | 10 | 5 | Flag owners, everything in Team | | Business | $249/mo ($199 annual) | Up to 25 | 25 | Unlimited | SSO with your IdP, unlimited webhook endpoints, 90-day audit log | Free used to cover one person. It now covers three, so a small team can run flags in production together without paying anything. Team takes the $49 price Pro used to have, sized for up to five people and three projects. Scheduled changes are now part of the paid plans. A lot stays on every plan, Free included. Targeting and percentage rollouts, all 13 SDKs, the Management API, the MCP server, the Terraform provider and the audit log. Expiry dates, stale flag detection and the cleanup Action, too, since flag debt starts on day one whatever your team size. Past 25 people, Enterprise is priced by quote. See [pricing](/pricing/) for the full grid. ## 8. If you already pay for Featureflip Your price doesn’t move. Pro and Business subscriptions that started before this change keep the rate they signed up at, and Pro subscribers get flag owners at that rate. The old price is tied to your current plan and billing interval. Switching from monthly to annual billing (or back again) moves the subscription to today’s price for that plan, and so does moving to a different plan. Stay where you are and nothing changes, and we won’t move anyone over from our side. ## The shorter version - Any flag can carry an expiry date on any plan. Once it passes, the flag is marked stale and keeps serving what it served. - A flag that expires or goes dead triggers one cleanup email, to its owner on Pro and Business and to your Owners and Admins otherwise. You can opt out per person. - Pro and Business name an owner per flag, and removing a member asks who inherits their flags. - Outbound webhooks send signed requests to your endpoint when flags or configuration change. - Business adds SSO through Okta, Entra ID or any OIDC provider, with optional enforcement. - Free covers 3 people. Team is $49, Pro $99, Business $249. Existing subscribers keep their price until they change plan or billing interval. ## Frequently asked questions ### Does a flag stop working when its expiry date passes? No. Evaluation ignores the date completely, and the flag serves exactly what it served the day before. Passing the date marks the flag Stale with the reason `PastExpiry` and, on Team and above, queues a cleanup notice. To keep a flag longer, move the date or remove it, and the stale marking clears straight away. ### Who gets flag cleanup emails if nobody owns the flag? The organization’s Owners and Admins. That’s also where every notice goes on Team, where flag owners aren’t part of the plan. Each person can turn the notices off under Profile, then Email Preferences, or with the unsubscribe link in any notice. ### How often will I get emailed about the same flag? Once per episode. A flag that expires or goes dead gets one notice and then stays quiet while it remains in that state. If it becomes active again and later goes dead a second time, a new notice can go out, but never within 30 days of the last one for that flag. ### Is my current Pro or Business price going up? Not while you stay on the same plan and billing interval. Existing subscriptions keep their price. Switching between monthly and annual billing, or changing plan, moves you to the current price for the plan you pick. ### Is signing in with Google or GitHub the same as SSO? They’re separate features. Google and GitHub sign-in are on every plan and let each person use an account they already have. Single sign-on, on Business, connects Featureflip to your company’s identity provider over OIDC, so your IdP decides who can get in and you can require it for every member. Free covers up to three people, never expires, and includes expiry dates, stale flag detection and the cleanup Action. [Create a free account](https://app.featureflip.io/signup) and put an expiry date on the next flag you make. --- ## Source: https://featureflip.io/blog/openfeature-feature-flags/ # OpenFeature Feature Flags: Node, .NET, Python, Go, Java, PHP [Blog](/blog/) # OpenFeature Feature Flags: Node, .NET, Python, Go, Java, PHP OpenFeature feature flags in Node.js, .NET, Python, Go, Java and PHP: set up the Featureflip provider, map evaluation context, keep call sites vendor-neutral. July 31, 2026 · Updated September 17, 2026 · - openfeature - sdks - integrations A feature flag SDK reaches further into a codebase than almost anything else you install. The call sites end up everywhere. A route handler, a React component, a background worker, a one-off migration script. That is what makes the lock-in question a fair one to ask before you pick a vendor. Moving later means editing several hundred call sites that all name one company’s SDK, most of them in files nobody has opened in a year. OpenFeature is the answer the ecosystem settled on. It is a CNCF open standard that puts a single vendor-neutral evaluation API in front of whatever flag service you happen to use, with a swappable **provider** doing the translation behind it. Featureflip ships six: `@featureflip/openfeature-node` on npm for the OpenFeature Node.js server SDK, `Featureflip.OpenFeature` on NuGet for the .NET SDK, `featureflip-openfeature-provider` on PyPI for the Python SDK, `github.com/canopy-labs/featureflip-go-openfeature` for the Go SDK, `io.featureflip:featureflip-openfeature` on Maven Central for the Java SDK, and `featureflip/featureflip-openfeature` on Packagist for the PHP SDK. This post covers what the standard actually standardizes, how to wire each of them up, the one context detail that quietly breaks percentage rollouts when you get it wrong, and where the standard stops. **Key Takeaways** - **OpenFeature** is a CNCF standard for *evaluating* flags. Your code calls `getBooleanValue` against an OpenFeature SDK, and a provider behind it talks to one flag service. - Featureflip ships an **OpenFeature provider** for **Node.js**, **.NET**, **Python**, **Go**, **Java** and **PHP** today, with the other languages in progress. Setup is one line at startup in all six. - The provider is the only vendor-specific code in your application. Replacing it changes the backend without touching a single call site. - `targetingKey` maps to Featureflip’s `user_id` and drives rollout bucketing. Omit it and percentage rollouts serve the control variation to everyone, quietly. - The standard covers **evaluation only**. Creating flags, writing targeting rules, and archiving dead ones stay with each vendor’s own API. - Providers are common across established flag vendors, several with wider language coverage than ours. A standard only one vendor shipped would be pointless, so that is the system working. ## 1. What OpenFeature standardizes, and what it leaves alone Three pieces do the work, and the split between them is what makes the swap possible. The **OpenFeature SDK** for your language exposes the standard evaluation API and holds the evaluation context. Behind it sits a **provider**, a thin adapter that implements the spec against exactly one flag service, and your application picks which one at startup. Every call after that goes through the standard API and never names a vendor. The provider is the seam. Everything vendor-specific lives in one startup line, which is what turns a migration into a configuration change. Most write-ups stop there. OpenFeature standardizes the *read* path, and only that. Flag creation, targeting rules, environment toggles and archiving all stay with each vendor’s own API, because there is no cross-vendor agreement on what a targeting rule even looks like. Your code becomes portable and your flag inventory does not, a distinction the marketing around the standard tends to blur. For Featureflip that management side lives in the [public REST API](/blog/feature-flag-management-api/) , and if you want an agent doing the work there is an [MCP server that hands flag management to Claude Code or Cursor](/blog/feature-flags-mcp-server/) . Neither is part of the standard, so both are work you would repeat against a different vendor. One more thing worth saying plainly. OpenFeature support is common across established flag vendors, and several ship providers for more languages than we do. A standard only one vendor implemented would be worthless, so that is the system working as designed. The portability argument works against us too, and we would rather put it here than let you find it on a [vendor comparison page](/compare/) . ## 2. Node.js, in one startup call Install the OpenFeature server SDK, the Featureflip SDK, and the provider that joins them: ```bash npm install @openfeature/server-sdk @featureflip/node @featureflip/openfeature-node ``` Both `@openfeature/server-sdk` and `@featureflip/node` are peer dependencies, so your application controls their versions and installs a single copy of each. That single-copy rule does real work, and section 2 of the [OpenFeature integration guide](/docs/integrations/openfeature/) explains why: one copy of `@featureflip/node` is what lets the provider and any direct SDK usage share the same underlying client core. Setting the provider is one call: ```ts import { OpenFeature } from '@openfeature/server-sdk'; import { FeatureflipProvider } from '@featureflip/openfeature-node'; await OpenFeature.setProviderAndWait( new FeatureflipProvider({ sdkKey: 'your-server-sdk-key' }), ); const client = OpenFeature.getClient(); const enabled = await client.getBooleanValue( 'new-checkout', false, { targetingKey: 'user-42', plan: 'pro' }, ); ``` `setProviderAndWait` blocks until the flag configuration has loaded, which is what you want at boot. Every read after that is answered from memory inside your process, so the standard API costs you nothing on the request path. The provider owns the underlying Featureflip client. If you also need the raw SDK, for a `track()` call outside OpenFeature or anything else the standard does not cover, call `FeatureflipClient.get()` with the same SDK key and both handles share one client core. You can also build the client yourself and pass the instance to the provider constructor instead of a config object. Either way you end up with a single connection and a single cached config, which is what you want when two parts of the same process both read flags. ## 3. .NET, the same shape Three packages, same as Node: ```bash dotnet add package OpenFeature dotnet add package Featureflip.Client dotnet add package Featureflip.OpenFeature ``` And the same one-call setup: ```csharp using OpenFeature; using Featureflip.OpenFeature; await Api.Instance.SetProviderAsync(new FeatureflipProvider("your-server-sdk-key")); var client = Api.Instance.GetClient(); var enabled = await client.GetBooleanValueAsync("new-checkout", false, EvaluationContext.Builder().SetTargetingKey("user-42").Build()); ``` The constructor takes a server SDK key with optional `FeatureFlagOptions`, or an existing `IFeatureflipClient` if you already build one in your DI container. Context mapping and evaluation reasons match the Node provider exactly. That matters for a team running more than one of them, since the mapping only has to be learned once. Two .NET-specific limits will catch you eventually. The .NET SDK has no custom-event API, so OpenFeature’s `Track()` quietly does nothing there. It will not throw. Nothing tells you the call went nowhere. And object flags accept objects and arrays only, so a Json flag holding a bare string or number resolves as `TYPE_MISMATCH` and you get your default back. The [.NET SDK reference](/docs/sdks/csharp/) covers the native surface underneath if you need the parts the standard does not reach. ## 4. Python, where one word does the waiting One package, unlike the other two. `featureflip` and `openfeature-sdk` are ordinary dependencies here rather than peer dependencies, so pip resolves them for you: ```bash pip install featureflip-openfeature-provider ``` The setup call is the same shape, with one word that decides whether it works: ```python from openfeature import api from openfeature.evaluation_context import EvaluationContext from featureflip_openfeature import FeatureflipProvider api.set_provider_and_wait(FeatureflipProvider(sdk_key="your-server-sdk-key")) client = api.get_client() enabled = client.get_boolean_value( "new-checkout", False, EvaluationContext(targeting_key="user-42", attributes={"plan": "pro"}), ) ``` `set_provider_and_wait`, not `set_provider`. The plain form runs `initialize()` on a background thread and returns straight away, so an evaluation on the next line can fire before any configuration has arrived and hand back your default instead. Same trap as `setProvider` in Node, except here the wrong one has the shorter name. Numbers are the other Python-specific wrinkle. Node has a single numeric accessor. Python splits it into `get_integer_value` and `get_float_value`. Both reject booleans, which reads as pedantry until you remember `bool` subclasses `int` in Python, so without that exclusion a boolean flag would satisfy `get_integer_value` and return `1`. Whole-number floats like `1.0` do pass the integer accessor, because JSON cannot tell `1` from `1.0` and neither can the guard. Ownership follows the .NET provider rather than Node: the provider closes the client only when it built one itself. A client you pass in through `client=` stays yours. Configuration-change events need `featureflip` 2.7.0 or newer, the release that added the SDK’s update hook. The [Python section of the integration guide](/docs/integrations/openfeature/) has the full type table. ## 5. Go, where the read is spelled twice One module, and the OpenFeature SDK alongside it: ```bash go get github.com/canopy-labs/featureflip-go-openfeature ``` The setup call has the same shape as the other three: ```go import ( featureflip "github.com/canopy-labs/featureflip-go-openfeature" "github.com/open-feature/go-sdk/openfeature" ) if err := openfeature.SetProviderAndWait(featureflip.NewProvider("your-server-sdk-key")); err != nil { log.Fatal(err) } client := openfeature.NewClient("my-app") enabled, err := client.BooleanValue(ctx, "new-checkout", false, openfeature.NewEvaluationContext("user-42", map[string]any{"plan": "pro"})) ``` `SetProviderAndWait`, not `SetProvider` — the same trap as Node and Python, for the same reason. The plain form initializes on a background goroutine and returns before any configuration has arrived. The Go-specific wrinkle is that the client spells the boolean read twice. `BooleanValue` returns `(bool, error)`; `Boolean` returns a plain `bool` and swallows the error. Both resolve identically through the provider, so the choice is only about whether you want to see a resolution failure or quietly take your default. Worth knowing if you use Featureflip’s [flag cleanup Action](/docs/integrations/flag-cleanup-action/) : it rewrites the single-value form, because standing a literal in for a two-value call would leave `enabled, err := true`, which does not compile. If you import the Featureflip SDK directly as well, alias one of them — both packages are named `featureflip`. Ownership follows .NET and Python: `Shutdown` closes the client only when the provider built it. Configuration-change events need the Go SDK 2.8.0 or newer. ## 6. Java, where the type check moves up a layer One artifact, and the OpenFeature SDK rides along with it: ```groovy dependencies { implementation 'io.featureflip:featureflip-openfeature:0.1.0' } ``` The setup call looks like the other four: ```java import dev.openfeature.sdk.*; import io.featureflip.openfeature.FeatureflipProvider; OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new FeatureflipProvider("your-server-sdk-key")); Client client = api.getClient(); boolean enabled = client.getBooleanValue("new-checkout", false, new ImmutableContext("user-42", Map.of("plan", new Value("pro")))); ``` `setProviderAndWait` again, not `setProvider`. The plain form returns before any configuration has arrived, which is the same trap every other runtime on this page has. The Java-specific wrinkle is that the provider cannot use the SDK’s typed accessors. The reason never shows up at the call site: `boolVariation` and its siblings report a wrong-typed flag exactly the way they report a genuine evaluation failure, with reason `ERROR`. Fine when you are reading a flag you wrote yourself. A provider cannot live with it, because OpenFeature wants those two cases back as `TYPE_MISMATCH` and `GENERAL`, and once the SDK has folded them together nothing is left to tell them apart with. So the provider reads values untyped and does the type check a layer up, where it still knows which type you asked for. A string flag read as a boolean now says so. Ownership follows the other providers. `shutdown()` closes the client only when the provider built it, so a `FeatureflipClient` you hand in yourself keeps running. A failed initial load will not fail `setProviderAndWait` either. The SDK carries on retrying in the background and evaluations serve your defaults until flags land, rather than the provider refusing to start over a bad thirty seconds. ## 7. PHP, where the client is yours to build The setup diverges before the first line of application code runs: ```bash composer require featureflip/featureflip-openfeature ``` ```php use Featureflip\FeatureflipClient; use Featureflip\Config; use Featureflip\OpenFeature\FeatureflipProvider; use OpenFeature\OpenFeatureAPI; $client = FeatureflipClient::get('your-server-sdk-key', new Config( cache: $psrCache, httpClient: $psrHttpClient, requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory, )); OpenFeatureAPI::getInstance()->setProvider(new FeatureflipProvider($client)); $enabled = OpenFeatureAPI::getInstance() ->getClient() ->getBooleanValue('new-checkout', false, $context); ``` setProvider(new FeatureflipProvider($client));$enabled = OpenFeatureAPI::getInstance() ->getClient() ->getBooleanValue('new-checkout', false, $context);"> Every other provider on this page takes a server SDK key and fills in the rest itself. PHP does not, because the underlying SDK has no defaults to pick from. Its core requires a caller-supplied PSR-18 HTTP client, PSR-17 request and stream factories, and a PSR-16 cache, and throws without them, so a key-only constructor would have to choose all three on the application’s behalf. Building the client yourself and handing it to the provider is the idiomatic shape in PHP anyway, where a service container already owns objects like this one, and an application reading flags both directly and through OpenFeature can share a single client between them. The provider never closes what it did not create, so `$client->close()` at shutdown stays the caller’s job. `resolveIntegerValue` accepts a whole-number float (`1.0`) as well as an integer, since JSON draws no line between them, and narrows the matched value to a real `int` before returning it. Objects are stricter: `resolveObjectValue` takes objects and arrays and nothing else, the same guard every other provider on this page enforces. PHP is the one provider here with no configuration-change event to react to. The `open-feature/sdk` package for PHP has no `PROVIDER_CONFIGURATION_CHANGED` to subscribe to at all, so a PHP application learns about a flag change the way it always has: on the underlying SDK’s next poll. ## 8. The context detail that breaks rollouts quietly OpenFeature’s evaluation context has one blessed field, `targetingKey`, plus any attributes you want to attach. Featureflip maps it like this: | OpenFeature context | Featureflip context | | --- | --- | | targetingKey | user_id, used for rollout bucketing | | Any other attribute | Passed through unchanged | Attributes passing through unchanged is the easy half. A targeting rule written against `plan` or `country` or `org_id` in the dashboard keeps working when the value arrives through an OpenFeature context, because the provider does not rename or reshape anything it was not asked to. The `targetingKey` half is where the trap lives. That key is what [percentage rollouts](/glossary/percentage-rollout/) hash to decide which side of the split a user lands on, and it is what makes that decision stick as you ramp from 10% to 50%. An explicit `user_id` or `userId` attribute takes precedence over `targetingKey` when both are present, which is handy during a migration off the native SDK. Supply neither and there is nothing to hash. A percentage rollout bucketed by user then serves the control variation to every caller. No exception is thrown and no warning is logged, because a keyless context is a legitimate thing to send for a flag that does not bucket by user. The ramp just sits at zero no matter how high you set the dial. If a rollout looks inert, check the context for a key before you check anything else, and the [sticky bucketing](/glossary/sticky-bucketing/) entry covers why a stable identifier is load-bearing here. ## 9. Reasons, variants, and reacting to change Every evaluation comes back with a reason attached, which is the field you actually reach for at 2am when a flag is the suspect. Featureflip’s outcomes map onto the standard vocabulary: | Featureflip outcome | OpenFeature reason | errorCode | | --- | --- | --- | | Targeting rule matched | TARGETING_MATCH | | | Fallthrough serve | DEFAULT | | | Flag disabled in environment | DISABLED | | | Prerequisite not met | PREREQUISITE_FAILED | | | Flag not found | ERROR | FLAG_NOT_FOUND | | Wrong value type requested | ERROR | TYPE_MISMATCH | | Evaluation error | ERROR | GENERAL | The matched [variation](/glossary/variation/) key comes back as `variant` on every provider. On five of the six, `flagMetadata` also carries `ruleId` and `prerequisiteKey`, so you can trace a decision back to the exact rule or the exact parent flag that gated it. The PHP provider is the exception: the OpenFeature PHP SDK it builds on has no flag-metadata API yet. Nothing else about [flag evaluation](/glossary/flag-evaluation/) becomes a black box because you went through the standard. Configuration changes surface as OpenFeature events. The initial load fires `PROVIDER_READY`. Every change after that fires `PROVIDER_CONFIGURATION_CHANGED`, carrying `flagsChanged` with the affected keys batched into one event: ```javascript import { OpenFeature, ProviderEvents } from "@openfeature/server-sdk"; OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, (details) => { console.log("flags changed:", details?.flagsChanged); }); ``` { console.log("flags changed:", details?.flagsChanged);});"> The other four emit the same event, each through its own SDK’s handler API: .NET through `Api.Instance.AddHandler` with `ProviderEventTypes.ProviderConfigurationChanged`, Python through `api.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ...)`, Go through `openfeature.AddHandler(openfeature.ProviderConfigChange, ...)`, and Java through `client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ...)`. PHP is the exception section 7 covered: its OpenFeature SDK has no events API to emit through. The definition of a change here is deliberately wide. A flag is reported when it is created, deleted or redefined. It is also reported when a segment its targeting rules reference changes, and when a flag it lists as a prerequisite changes, because both of those alter what it evaluates to even though the flag itself was never edited. A cache keyed on flag identity would miss those two cases and go stale without ever knowing. ## 10. Where the standard stops Four boundaries to know before you commit to the standard. **Language coverage is partial.** Node.js, .NET, Python, Go, Java and PHP have providers today. The other languages are in progress, and until one ships, those services use the native [language SDKs](/docs/sdks/overview/) , which is a perfectly good place to be. Mixed estates work fine: a Node service can go through OpenFeature while a Go service uses the native SDK against the same flags, because both resolve against one shared evaluation engine. **The standard covers the common surface.** Booleans, strings, numbers, objects, context, reasons, events. Anything past that is vendor territory, which is why the escape hatch of reaching for the underlying client matters and why every provider makes it easy. **Object flags want objects.** Arrays and objects resolve normally. A Json flag holding a bare primitive comes back as `TYPE_MISMATCH` with your default, in every runtime. **Management stays vendor-specific.** Repeating this from section 1 because it is the boundary people are most surprised by. The standard makes your reads portable. Your flag inventory, rules, and environments still live in one vendor’s system, and moving them is an export and an import. None of this sits behind a plan. The providers are Apache 2.0 licensed and work on the Free plan, so trying Featureflip through the standard costs nothing and commits you to nothing beyond an `npm install`. If you are still weighing the wider question, the [build versus buy analysis](/blog/build-vs-buy-feature-flags/) puts lock-in next to the other objections teams raise before they commit to a vendor. ## The shorter version OpenFeature is a CNCF standard that puts one vendor-neutral evaluation API in front of any flag service, with a swappable provider behind it. Featureflip ships providers for Node.js (`@featureflip/openfeature-node`), .NET (`Featureflip.OpenFeature`), Python (`featureflip-openfeature-provider`), Go (`featureflip-go-openfeature`), Java (`io.featureflip:featureflip-openfeature`) and PHP (`featureflip/featureflip-openfeature`), each set up in a single startup call, after which your code calls `getBooleanValue` and friends and never names a vendor again. `targetingKey` maps to Featureflip’s `user_id` and drives rollout bucketing, so omitting it silently serves the control variation to everyone. Other attributes pass through untouched, so dashboard targeting rules keep working. Reasons, variants and rule metadata all survive the trip on every provider but PHP’s, and configuration changes arrive as `PROVIDER_CONFIGURATION_CHANGED` events on the other five. The standard covers evaluation only, so flag management stays vendor-specific, and language coverage is Node, .NET, Python, Go, Java and PHP today with more in progress. ## Frequently asked questions ### What is OpenFeature? OpenFeature is an open specification, hosted by the Cloud Native Computing Foundation, for how applications evaluate feature flags. Your code calls a standard API against an OpenFeature SDK, and a provider plugged in behind it translates those calls for one specific flag service. Because your call sites target the standard rather than a vendor SDK, changing services becomes a configuration change rather than a rewrite. The [OpenFeature glossary entry](/glossary/openfeature/) has the short definition. ### Does Featureflip have an OpenFeature provider? Yes, six. `@featureflip/openfeature-node` connects the OpenFeature Node.js server SDK to Featureflip, `Featureflip.OpenFeature` does the same for the OpenFeature .NET SDK, `featureflip-openfeature-provider` for the Python one, `github.com/canopy-labs/featureflip-go-openfeature` for Go, `io.featureflip:featureflip-openfeature` for Java, and `featureflip/featureflip-openfeature` for PHP. Each is set with a single call at startup, and all six are available on the Free plan. Installation and context mapping for each are in the [integration docs](/docs/integrations/openfeature/) . ### Which languages have a Featureflip OpenFeature provider? Node.js, .NET, Python, Go, Java and PHP today. Providers for the other server languages are in progress. Every language Featureflip supports has a native SDK regardless, and both paths resolve flags against the same evaluation engine, so a mixed estate returns identical answers whether a service goes through OpenFeature or the [native SDK](/docs/sdks/overview/) . ### Can I switch feature flag vendors without changing code? Your evaluation code, largely yes. That is what the provider model is for: the vendor appears in one startup line, and replacing it repoints every call site at once. Your flag configuration is a separate question. OpenFeature does not standardize how flags are created, targeted or archived, so the inventory itself still has to be exported from one system and imported into the next. ### Does OpenFeature handle creating and managing flags? No. The standard covers evaluation only, which means reading a flag’s value for a given context. Creating flags, writing targeting rules, toggling environments and archiving shipped flags all stay with each vendor’s own interface, which for Featureflip means the dashboard, the [Management API](/docs/management-api/overview/) , or the MCP server. Featureflip is a focused, flat-priced feature flag platform with sub-millisecond local evaluation and streaming updates, and its OpenFeature providers let you get all of that through the CNCF standard instead of a proprietary SDK. Read the [OpenFeature integration guide](/docs/integrations/openfeature/) for the full context and reason mapping, browse the [Node SDK reference](/docs/sdks/nodejs/) for what sits underneath, compare plans on the [pricing page](/pricing/) , and [start on the Free plan](https://app.featureflip.io/signup) without a credit card. --- ## Source: https://featureflip.io/blog/prerequisite-flags/ # Prerequisite Flags: Gate One Flag on Another, No Glue Code [Blog](/blog/) # Prerequisite Flags: Gate One Flag on Another, No Glue Code Featureflip supports first-class prerequisite flags. A child flag stays off until its parent serves the right variation, with no application-side gating. May 27, 2026 · Updated June 4, 2026 · - prerequisites - dependencies - release There’s a pattern almost every flag-heavy codebase ends up with: one flag that should only be considered if another flag is already on. The new checkout flow needs the new payment form. The enterprise analytics view needs the customer to be on the enterprise plan. The migration to the new database backs the half-dozen features that read from it. Until now, that gating lived in your application code as an `if (parent) { check child }` wrapper, and it drifted across services as the parent’s targeting changed. As of this week, it doesn’t have to. Prerequisite flags are now first-class in Featureflip. A child flag declares the parent it depends on and the variation that parent must serve, and the platform enforces the dependency before any of the child’s targeting rules run. The gating logic moves out of your application and into the flag itself. **Key Takeaways** - Prerequisites are variation-match: the child only fires when the parent serves the specific variation you name. That makes them work for multivariate plan flags as well as on/off booleans. - The dependency lives in the platform, not in your code. Parent targeting changes automatically flow through to every dependent flag. - When a prerequisite gates a child off, the SDK tells you which parent did it. Debuggable from a single log line, no server round-trip. - For teams evaluating Featureflip against [LaunchDarkly](/compare/featureflip-vs-launchdarkly/) or [ConfigCat](/compare/featureflip-vs-configcat/) , prerequisite flags now migrate 1:1. ## Three places prerequisites earn their keep Most of the value lands in three patterns that come up over and over once a team has the construct. ### Coordinated feature rollouts You’re rolling out a new checkout flow. The new payment form, the redesigned receipt email, and the rewritten order confirmation page all need to fire together, or none of them should. Without prerequisites, each child flag has to duplicate the parent’s targeting (`country == US AND plan == enterprise AND beta_opt_in == true`), and the moment that targeting drifts in any one of the three, the user experience splinters. With prerequisites, the children declare `new-checkout-v2 = on` and the parent owns the rollout schedule on its own. When you ramp the parent from 1% to 10% to 100%, every dependent ramps with it automatically. ### Plan-tier entitlements A SaaS product typically has a flag per gated feature (advanced analytics, SSO, custom domains) plus a parent flag that names the plan tier. Each entitlement declares the plan-tier flag as a prerequisite. When a customer downgrades, the parent’s targeting flips, and every entitlement under it falls back to off in the same evaluation cycle. No per-flag plumbing, no nightly reconciliation job, no risk of one entitlement staying enabled because someone forgot to update its rule. Variation-match earns its design here: a multi-tier plan flag can have entitlements that require specifically `enterprise`, others that require `pro`-or-higher (via a separate parent), all on the same flag graph. ### Cutovers and kill switches Migrating from one backend to another, say a database, a payment processor, or a recommendation engine? Make the migration flag the parent, and every read-path and write-path feature that depends on the new backend declares the migration flag as a prerequisite. The cutover becomes one flip on the parent. The kill switch, rolling back to the old backend, is also one flip, and you don’t have to maintain N rollback procedures for the N features that touched the new path. ## How they work, briefly A prerequisite is two strings: the parent flag’s key, and the variation the parent must serve. Each flag environment configuration carries an ordered collection of them, alongside its targeting rules and fallthrough. When a child flag is evaluated, the platform walks its prerequisites in order. For each one, it evaluates the parent against the same user context, compares the served variation to the expected variation, and short-circuits to the child’s off variation if they don’t match. Only if every prerequisite passes does the evaluator run the child’s targeting rules. The same algorithm runs server-side in the Evaluation API and locally in every server-side SDK ([JS](https://www.npmjs.com/package/@featureflip/js) , Python, Go, Java, C#, Ruby, PHP), so a flag check returns the same answer no matter where it runs. A few guardrails are worth knowing about, even if you never touch them directly. Cycles like `A → B → A` are blocked at save time, with the offending chain printed back in the error. You can’t delete a flag (or remove a variation) that other flags depend on. The API names the dependents so you know exactly what to unwire first. And there’s a hard depth cap of 10 on prerequisite chains, well above what any realistic dependency graph needs, as a safety net. ## What it looks like from the SDK Most application code picks up prerequisite behavior automatically, because prerequisites are enforced inside the evaluator before any variation is selected, so `boolVariation`, `stringVariation`, and friends keep working unchanged. The new surface is the evaluation reason, which now tells you when a prerequisite gated a flag off and which parent did it: ```ts import { FeatureflipClient, createNodePlatform } from '@featureflip/js'; const client = FeatureflipClient.get( { sdkKey: process.env.FEATUREFLIP_SDK_KEY!, baseUrl: 'https://eval.featureflip.io' }, createNodePlatform(), ); await client.waitForInitialization(); const context = { user_id: 'user_42', plan: 'pro' }; const detail = client.variationDetail('beta-checkout', context, false); if (detail.reason === 'PrerequisiteFailed') { console.log(`beta-checkout gated off by ${detail.prerequisiteKey}`); } else if (detail.value === true) { renderNewCheckout(); } ``` That `prerequisiteKey` field is the piece that closes the debuggability gap. With the application-side workaround, the symptom of a misfiring gate was usually “the child flag isn’t doing what I expected”, and the engineer reading the dashboard saw a child whose rules all evaluated cleanly, with no indication that the real reason was something on a parent flag. The first-class construct names the parent explicitly, in the same evaluation detail your application already has access to. ## Editing prerequisites in the dashboard The flag editor in the web app exposes a “Prerequisites” section beneath the targeting rules. Each row is two dropdowns: pick the parent flag, then pick the variation it must serve. The variation list re-populates when you change the parent, so it never goes stale against the parent’s current variations. The two failure cases, cycles and dependent-blocking deletes, render inline instead of as opaque API errors. If you try to save a prerequisite that would create a loop, the form shows you the chain (`flag-a → flag-b → flag-a`) and refuses. If you try to delete a flag or remove a variation that other flags depend on, the response includes the dependent flag keys so you can navigate to each one and unwire it before retrying. ## Migrating from LaunchDarkly or ConfigCat Prerequisite flags were one of two surfaces that previously required hand-rework when migrating to Featureflip from either platform. That changes with this release. [LaunchDarkly’s prerequisite model](https://launchdarkly.com/docs/home/flags/prerequisites) is variation-match, the same shape as Featureflip’s. A migration script that walks a LaunchDarkly project export and produces equivalent Featureflip prerequisites is a couple of dozen lines of field-name translation, not a redesign. [ConfigCat’s prerequisite flag](https://configcat.com/docs/targeting/targeting-rule/flag-condition/) is also variation-match and migrates the same way. The one structural difference: ConfigCat supports an explicit “does not equal” comparator on prerequisites, while Featureflip only supports “equals” (matching LaunchDarkly’s semantics). If you have prerequisites in ConfigCat that use “does not equal”, you’ll express them in Featureflip by inverting the expected variation. That is usually a small refactor on a small subset of flags rather than a project-wide rewrite. If you’re still weighing where to land, our guide to [ConfigCat alternatives](/alternatives/configcat-alternative/) compares the migration path, pricing, and feature trade-offs across six platforms. For procurement-gated buyers this matters, because prerequisite flags were one of the named items on the “missing feature” list and no longer are. ## Frequently asked questions ### What’s the difference between a prerequisite and a targeting rule? A targeting rule decides what variation to serve based on context attributes (country, plan, user ID). A prerequisite decides whether to *consider serving anything other than off*, based on another flag’s result. Prerequisites run before targeting rules. If any prerequisite fails, the targeting rules never get a chance to fire. ### Can prerequisite flags create infinite loops? No. Cycles are detected at save time and rejected with the offending chain (`flag-a → flag-b → flag-a`) returned in the error response. There’s a depth cap of 10 on the evaluator as a defense-in-depth safety net, but the primary defense is structural: the API won’t accept a config that would create a loop. ### How deep can a prerequisite chain go? The hard limit is 10, and in practice most production flag graphs are 1–3 deep. If you’re approaching the limit, that’s usually a signal to flatten the dependency structure. One parent with several direct dependents tends to be cleaner than a long chain. ### What happens to dependent flags when I disable the parent? The dependent flags serve their off variations with the evaluation reason set to `PrerequisiteFailed` (or the equivalent per-SDK casing), and `prerequisiteKey` names the parent that gated the child off. This is the same behavior you’d get if the parent simply served a variation that the child’s prerequisite didn’t expect. ### Can I delete a flag that other flags depend on? Not until the dependents drop their references. The API returns a 400 with code `FLAG_HAS_DEPENDENTS` and the list of dependent flag keys, and the web UI renders the list so you can navigate to each one and remove the prerequisite. The same guard applies to removing a variation that other flags expect. ## Try it The fastest way to see prerequisites in action is to [start a project on the Free plan](https://app.featureflip.io/signup) , free forever for one project, create two flags, and reference one from the other in the new Prerequisites section of the flag editor. The full evaluation reason surfaces on `variationDetail` from the first flag check, so you can confirm the gating live without instrumenting anything. Prerequisites slot in alongside the rest of the [targeting and segmentation surface](/docs/concepts/targeting/) , and the [prerequisite flags documentation](/docs/concepts/prerequisites/) covers the evaluation order, variation matching, and guardrails in reference detail. The broader question of when to add a flag at all, and how to keep your flag practice healthy, is covered in [the broader best-practices pillar](/blog/feature-flag-best-practices/) , the [feature flag cleanup playbook](/blog/feature-flag-cleanup/) , and the [feature flag anti-patterns post](/blog/feature-flag-anti-patterns/) . --- ## Source: https://featureflip.io/blog/progressive-delivery-explained/ # Progressive Delivery Explained: The Two-Pillar Playbook [Blog](/blog/) # Progressive Delivery Explained: The Two-Pillar Playbook Progressive delivery has two pillars: release progression and progressive delegation. How feature flags turn a deploy into a dial you can turn up slowly. June 3, 2026 · - progressive-delivery - deployments - rollouts Most explanations of progressive delivery describe half of it. They cover canaries, rings, and percentage rollouts: the part where you widen a release slowly. Then they stop. The half they skip is the one that gives the idea its name. When RedMonk analyst James Governor coined “progressive delivery” in 2018, he defined it by *two* principles. The second one, handing release control to the person who owns the outcome, is what separates a delivery strategy from a deployment trick ([RedMonk](https://redmonk.com/jgovernor/2018/08/06/towards-progressive-delivery/) ). The reason this matters is blast radius. A big-bang release couples 100% of your users to a single moment in time. If the change is bad, everyone is already on it, and the only lever left is a redeploy. Progressive delivery breaks that coupling: expose the change to a sliver of traffic, watch, then widen. Make the widening a configuration change someone can reverse in one click. **Key Takeaways** - Progressive delivery exposes a release to users gradually, in controlled increments, instead of all at once. James Governor (RedMonk) coined the term in 2018 ([RedMonk](https://redmonk.com/jgovernor/2018/08/06/towards-progressive-delivery/) ). - It rests on two pillars: *release progression* (widen exposure at a safe pace) and *progressive delegation* (move release control to the outcome owner). Most write-ups cover only the first. - Feature flags are the control plane for both. They decouple deploy from release and make a rollback a config push, not a redeploy. - The payoff is smaller blast radius, faster detection, and faster recovery, which are the levers behind the metrics DORA tracks. **How to use this guide.** This is the conceptual pillar: the full mental model, the technique rubric, and where flags fit. For the one-paragraph definition, see the [progressive delivery glossary entry](/glossary/progressive-delivery/) . For the rule-by-rule treatment of rollouts in a wider flag practice, see [feature flag best practices](/blog/feature-flag-best-practices/) . For the failure mode it replaces, see the big-bang release in [feature flag anti-patterns](/blog/feature-flag-anti-patterns/) . ## 1. What progressive delivery actually means Progressive delivery is the practice of exposing a new release to production users in controlled increments: internal staff first, then a small canary, then a wider beta cohort, then everyone, with a checkpoint at each step. It isn’t a single tool. It’s an umbrella over a basket of techniques: canary releases, feature flags, [dark launches](/glossary/dark-launch/) , A/B testing, and blue-green deploys ([RedMonk](https://redmonk.com/jgovernor/2018/08/06/towards-progressive-delivery/) ). The term has a specific origin. James Governor of RedMonk coined it in mid-2018 after hearing Sam Guckenheimer describe Microsoft’s “progressive experimentation” model, and developed the idea with Adam Zimman, then at LaunchDarkly ([SD Times](https://sdtimes.com/devops/progressive-delivery-testing-software-through-limited-releases/) ). The framing has held up well enough that it now has a canonical book, *Progressive Delivery: Build the Right Thing for the Right People at the Right Time* (Governor, Harrison, Waterhouse, and Zimman, [IT Revolution](https://itrevolution.com/product/progressive-delivery/) , 2025). The mental model worth keeping is simple: deploy is not release. Deploying ships code to a server. Releasing decides who runs it. Progressive delivery is everything that happens between those two events. The more you can separate them, the less a deploy has to be an act of faith. ## 2. The two pillars: release progression and progressive delegation Governor’s definition has two parts, and you need both to get the benefit. **Release progression** is adjusting how many people can see a change, at a pace the business can tolerate: beta users first, then a trusted group, then everyone. **Progressive delegation** is the under-told half. It means shifting release control out of the engineering and operations org and into the hands of the business or product owner accountable for the outcome ([SD Times](https://sdtimes.com/devops/progressive-delivery-testing-software-through-limited-releases/) ). Why does the second pillar matter so much? Because a canary that only the platform team can advance is still a *deployment* strategy. It tells you whether the new version is healthy; it doesn’t tell you whether the feature is right for the audience. Delegation hands the dial to the person who can actually answer that: the PM who knows the beta cohort, the support lead watching ticket volume, the growth team running the experiment. The release stops being an ops event and becomes a product decision. Each ring widens the blast radius on purpose. A regression caught at the canary ring touches 1–5% of users, not all of them, and reversing it is a flag change, not a redeploy. IT Revolution has since published a “Four A’s” framework that extends the model for teams formalizing the practice ([IT Revolution](https://itrevolution.com/articles/the-four-as-a-framework-for-progressive-delivery/) ). But the two pillars are the load-bearing idea: widen exposure carefully, and let the right person own the widening. ## 3. Progressive delivery vs continuous delivery, canary, and blue-green These terms get used interchangeably, and they shouldn’t be. **Continuous delivery** keeps a change in a *deployable* state at all times. It’s about the pipeline. **Progressive delivery** governs *who sees the change* after it’s deployed. It’s about exposure. **Canary** and **blue-green** are deployment-routing techniques. **Feature flags** are an application-layer technique. Progressive delivery is the strategy that orchestrates whichever of those you use. A concrete way to keep them straight: continuous delivery gets the artifact to the door, progressive delivery decides how far it walks in. You can practice continuous delivery and still do big-bang releases, shipping to 100% the instant the pipeline goes green. Progressive delivery is the discipline you add on top to stop doing that. The mechanic that ties it together is rollback economics. With a redeploy-based strategy, reverting means rebuilding and shipping again. With traffic routing, reverting means shifting weight back ([Argo Rollouts](https://argo-rollouts.readthedocs.io/en/stable/features/canary/) ). With a flag, reverting means flipping a value in config. That difference is the whole game: it cuts mean time to recovery, and because a regression first shows up against a small traffic slice, it cuts mean time to *detection* too. Why flags change the economics: rollback stops being a deploy. Times are typical orders of magnitude, not benchmarks. ## 4. The techniques that implement it Progressive delivery is assembled from a handful of techniques, and the useful question isn’t “which is best” but “which risk am I managing.” Infrastructure and version risk point toward canary releases. Feature-level risk, like a new checkout flow that might be wrong, points toward flags. Validating performance before anyone sees a UI points toward dark launching. Here’s the map: | Technique | Best when… | Blast radius at step 1 | Rollback | | --- | --- | --- | --- | | Dark launch | validating load or performance before users see anything | 0% (no UI exposed) | instant (turn the flag off) | | Canary release | infrastructure or version risk; metric-gated promotion | small traffic slice | reroute traffic | | Percentage rollout (flags) | feature-level risk, per-user consistency needed | 1–5% of users | flag config push | | Ring-based | org-structured exposure (internal → beta → GA) | one ring at a time | flag or segment change | | Blue-green | all-or-nothing cutover with a fast revert path | 0%, then 100% on cutover | swap back to the old environment | Most real rollouts combine these. A team might dark-launch a new pricing service to validate it under production load, then put the user-facing change behind a percentage rollout once the backend is proven. The opposite of all this is the [big-bang release](/blog/feature-flag-anti-patterns/) : ship to everyone at once and hope. That’s exactly the pattern progressive delivery exists to retire. The rings model maps cleanly onto these techniques. Each ring is a population (staff, canary, beta, broad, GA), and the technique you reach for decides *how* you route to that population: traffic weight for canary, a flag rule for a percentage rollout, a segment match for ring-based exposure. Plenty of teams run the beta ring as a named cohort of opt-in testers instead of a percentage band, which changes how the ring is widened and who sits in it. [Running a beta as an opt-in cohort](/use-cases/beta-programs/) covers that variant. ## 5. How feature flags become the control plane Feature flags are the mechanism that makes both pillars cheap, which is why they show up in nearly every progressive delivery story. They decouple deploy from release: ship the code dark, then turn it on when you choose. They make exposure a configuration value rather than a code change. A [percentage rollout](/docs/concepts/rollout-strategies/) or a [targeting rule](/docs/concepts/targeting/) is something you edit, not redeploy. And they make delegation safe, because flipping a flag in a dashboard is a far smaller blast radius than handing someone access to a deploy pipeline. In code, the feature is just a branch behind a named decision: ```js // The rollout percentage and targeting live in config, not here. if (client.boolVariation('checkout-v3', user, false)) { renderCheckoutV3(); } else { renderCheckoutV2(); } ``` The widening happens in the dashboard: 1%, then 5%, then 20%, then 100%. The code never changes between steps, and neither does the deploy. That’s the property that makes the rings chart above a configuration timeline rather than a deploy schedule. This is the rung Featureflip occupies. Flags evaluate in-process in sub-millisecond time, configuration changes stream to every connected SDK, and a [kill switch](/glossary/kill-switch/) propagates fleet-wide without a deploy, so a rollback is a config push, not a release. Automated, metric-gated canary *analysis* is a complementary rung that teams layer on top of the flag control plane. That’s the kind that promotes or aborts a rollout based on live error budgets, which tools like Argo Rollouts and Flagger run at the infrastructure layer. Knowing which rung you’re standing on keeps the architecture honest. For the rule-by-rule version of rolling out well, see [feature flag best practices](/blog/feature-flag-best-practices/) . ## 6. Why it matters: the DORA case Progressive delivery serves the metrics the DevOps research community has spent a decade measuring. It shrinks the *impact* of a change failure by shrinking the blast radius, it speeds recovery because a rollback is a flag flip, and it de-risks the higher deployment frequency that correlates with high-performing teams ([DORA](https://dora.dev/guides/dora-metrics-four-keys/) ). The consistent finding across years of the research is counter-intuitive but well-established: the teams that deploy most often also fail least often. Speed and stability rise together, and the practices that allow that are the ones that make any single change reversible. The 2025 State of DevOps report reshaped how it presents this. It moved away from the old elite/high/medium/low ladder toward seven team archetypes and per-metric buckets, and split its measures into throughput and instability groups. It also promoted “rework rate”, the share of deployments made to fix a user-visible problem, to a headline signal it had introduced in 2024 ([DevOps.com](https://devops.com/dora-2025-faster-but-are-we-any-better/) ; [CD Foundation](https://cd.foundation/blog/2025/10/16/dora-5-metrics/) ). The same report flags a sharp edge: AI-assisted teams are pushing throughput up but driving change failure rate up too, wherever their feedback loops can’t keep pace with the new velocity ([DevOps.com](https://devops.com/dora-2025-faster-but-are-we-any-better/) ). That last point is the whole argument for progressive delivery in one line. A feedback loop *is* what it provides: a small, watched slice of traffic before the full release. If you’re shipping faster than ever, the question isn’t whether you can deploy quickly; it’s whether you can catch a bad change before it reaches everyone. Decoupling deploy from release, which is where this all started in [the boundary between deploying and releasing](/blog/feature-flags-vs-environment-variables/) , is what makes the loop possible. ## 7. How to start without a platform team You don’t need Kubernetes, a service mesh, or an SRE rotation to start. The minimum viable progressive delivery loop is five steps: ship the change behind a flag (off), enable it for internal staff, expand to a small percentage of users, watch the dashboards you already have, then widen or roll back. That’s release progression with the tools most teams already own. Start with one genuinely risky feature rather than mandating the practice everywhere at once. Run it through the rings (internal, canary, beta, GA), and resist the urge to skip steps when the early signals look good. The point of the loop is the steps you take when they *don’t*. Add the delegation pillar once the loop is trusted: let the PM or feature owner control the dial, so the release becomes their decision instead of a ticket they file with engineering. One caution carries over from flag practice generally. A rollout flag is a temporary flag. Once you’ve stabilized at 100% for a release cycle or two, remove it. Otherwise today’s progressive delivery is next quarter’s flag debt. The mechanics of doing that safely live in the [feature flag cleanup playbook](/blog/feature-flag-cleanup/) , and the broader discipline across all thirteen of our [SDKs](/docs/sdks/overview/) is built to make the loop boring in the best way. ## The shorter version Progressive delivery is the gradual, controlled exposure of a release to users instead of an all-at-once cutover. James Governor coined the term at RedMonk in 2018 and defined it by two pillars: release progression, which widens who can see a change at a safe pace, and progressive delegation, which hands control of that widening to the person who owns the outcome. Most write-ups cover only the first. The techniques that implement it (dark launch, canary, percentage rollout, ring-based exposure, blue-green) are chosen by the risk you’re managing, and feature flags are the control plane that makes all of them cheap by turning a release into a configuration change with a one-click rollback. The payoff lands directly on the metrics DORA tracks: smaller blast radius, faster detection, faster recovery, and the confidence to deploy more often. You can start without a platform team: ship dark, enable for staff, widen by percentage, watch, and roll back with a flag flip. Then add delegation once the loop earns trust. And remember to remove the rollout flag once the feature is fully live, before it becomes debt. ## Frequently asked questions ### What is progressive delivery? Progressive delivery is the practice of exposing a release to users gradually, in controlled increments (internal staff, then a canary, then beta, then everyone) rather than all at once. RedMonk analyst James Governor coined the term in 2018, defining it by two pillars: release progression and progressive delegation ([RedMonk](https://redmonk.com/jgovernor/2018/08/06/towards-progressive-delivery/) ). ### What’s the difference between continuous delivery and progressive delivery? Continuous delivery keeps a change deployable at any time. It’s about the pipeline. Progressive delivery governs who sees the change after it’s deployed. It’s about exposure. You can practice continuous delivery and still do big-bang releases; progressive delivery is the discipline added on top to widen exposure in controlled steps instead. ### Is canary deployment the same as progressive delivery? No. A canary release is one technique; progressive delivery is the broader strategy that may use canary, feature flags, ring-based exposure, dark launches, or blue-green deploys together ([RedMonk](https://redmonk.com/jgovernor/2018/08/06/towards-progressive-delivery/) ). A canary tells you whether a version is healthy; progressive delivery also covers who controls the rollout and how it widens. ### How do feature flags enable progressive delivery? Feature flags decouple deploy from release: you ship code dark, then turn it on by configuration. They make exposure a value you edit, like a [percentage rollout](/glossary/percentage-rollout/) or targeting rule, rather than a redeploy. They make rollback a one-click config push. That’s what turns a rollout into a dial you can turn up slowly and reverse instantly. ### What are the two principles of progressive delivery? Release progression and progressive delegation. Release progression is widening who can see a change at a pace the business can tolerate. Progressive delegation is shifting release control from engineering and operations to the business or product owner accountable for the outcome ([SD Times](https://sdtimes.com/devops/progressive-delivery-testing-software-through-limited-releases/) ). Most explanations cover the first and skip the second. Featureflip gives you the control plane for progressive delivery: percentage rollouts, targeting rules for internal and beta cohorts, sub-millisecond local evaluation, and a kill switch that streams to every connected SDK in sub-second, so widening a release, or reversing it, is a configuration change rather than a deploy. If you’d like to run the loop in this guide on your own features, [start with the Free plan](https://app.featureflip.io/signup) . --- ## Source: https://featureflip.io/blog/self-hosted-feature-flags/ # Self-Hosted Feature Flags: When Running Your Own Wins [Blog](/blog/) # Self-Hosted Feature Flags: When Running Your Own Wins Self-hosted feature flags trade a license fee for an availability problem: your kill switch ends up inside the blast radius it exists to protect you from. August 4, 2026 · - self-hosting - architecture - best-practices Self-hosted feature flags put the flag service on infrastructure you own, which means you run the server, the database behind it, the upgrade path, and the pager that fires when it stops answering. The alternative is a hosted service where somebody else does that and you pay them. Most comparisons of the two settle into an argument about money, license fee against engineering hours, roughly, with a nod at data residency. That argument is real and it is also the easy half. The harder question is what each model does to your ability to turn a feature off during an incident, and the answer is less intuitive than it looks, because modern flag SDKs already changed where the uptime requirement sits. **Key Takeaways** - Local-evaluation SDKs mean a flag service outage freezes propagation while evaluation carries on from cache. - You lose the ability to change anything during that outage, which is exactly what an incident needs. - Self-hosting inside your production cluster puts the kill switch in the blast radius it exists to protect you from. - Data residency, air-gapped networks, and regulated workloads are the cases where self-hosting wins outright. - If you self-host, run the flag plane somewhere your application failures cannot reach. ## 1. What you are actually choosing between A flag platform has two halves that fail independently. The control plane is where flags are created, targeted, and toggled. Dashboard, API, database, audit log. It is a fairly ordinary web application. Its availability requirement is that of any internal tool. The data plane is the part that answers “what value does this user get for this flag”. In a local-evaluation architecture, that answer comes from memory inside your own process. The [SDK](/docs/sdks/overview/) holds a copy of the flag configuration, keeps it current over a streaming or polling connection, and evaluates against it without a network call. Evaluating a flag costs a map lookup and some rule matching. Self-hosting means taking on the control plane. It is worth being precise about that, because the phrase “self-hosted feature flags” gets used as though it meant your application stops depending on anything external, and with local evaluation that dependency was already gone. This is a different question from whether to build a flag system yourself. That one is covered in [the build-versus-buy breakdown](/blog/build-vs-buy-feature-flags/) , and the answer there turns on maintenance slope and opportunity cost. Here you have already decided to adopt a platform. The only question left is who operates it. ## 2. Local evaluation already decoupled uptime When the flag service becomes unreachable, a local-evaluation SDK does not start failing. It has the configuration in memory. Every `variation()` call keeps returning the same values it was returning a second earlier, at the same speed, with no errors and no timeouts. Your checkout flow does not care that a server somewhere stopped answering. Propagation stops. New flag changes have nowhere to arrive from. The config in memory is frozen at whatever it said when the connection dropped, and it stays frozen until the connection comes back. A flag service outage freezes configuration. Evaluation keeps running from the in-memory cache, which is why "the flag service went down" rarely means "the site went down". Recovery is where implementations differ, and it is worth checking before you commit to any of this. An SDK that treats a connection error as terminal will sit there serving stale config long after the service came back, and you find out during the next deploy. We wrote up [what makes recovery the hard part](/blog/feature-flag-outage-recovery/) after auditing our own SDKs for exactly that failure. ## 3. The control plane sits inside your blast radius Now put the two facts together. During that outage you can still evaluate flags, and you cannot change them. The moment you most need to change one is during an incident, when something is on fire and the kill switch is the fastest thing you own. Self-hosting is what decides whether those two events are correlated. Run the flag service in the same Kubernetes cluster as your application, and a bad day for that cluster is a bad day for both. Control-plane pressure, a node pool exhausted, a networking change that partitions services from each other, a database at connection limits. Any of those can take out your application and your flag service in the same minute. You reach for the [kill switch](/glossary/kill-switch/) and the dashboard will not load. A hosted control plane sits outside that boundary by construction, running on someone else’s infrastructure in a failure domain that has nothing to do with your cluster. It has its own outages and those are entirely outside your control, which is a real cost you are accepting. Its bad days will not coincide with yours, though, because nothing connects the two. The mitigation, if you self-host, is to stop treating the flag service as just another workload. Separate cluster at minimum, and a separate account or region is better still. Anything that keeps the plane you use to fix production from depending on production. Teams that self-host well already do this. Teams that self-host because it was the default deployment in the docs usually have not thought about it, and they discover the coupling at the worst possible moment. ## 4. When self-hosting is the right call Plenty of situations point the other way, and they tend to be about data rather than money. **Evaluation context cannot leave your perimeter.** This is the strongest case. If the attributes you target on are regulated health data, classified material, or anything under a residency rule with real teeth, then sending them to a third party is off the table regardless of certifications. Local evaluation narrows what crosses the boundary here without closing it, and the difference is worth checking rather than assuming. A server SDK evaluates inside your process, so the targeting attributes never leave for the decision itself, though analytics events typically carry the user identifier back to the vendor afterwards. Client SDKs invert that entirely and send the evaluation context to the service to be evaluated there. Featureflip works this way too, so if your residency rule covers identifiers rather than only payload data, that is the specific thing to verify on any vendor including us. **The network is air-gapped.** With no outbound path there is no hosted option to evaluate, whatever its merits elsewhere. **Procurement will not clear a new vendor.** Sometimes the blocker is organizational rather than technical, and standing up an open-source server beats waiting out a nine-month security review. **You already run this class of service well.** A platform team with an existing on-call rotation, a paved path for stateful services, and a habit of upgrading things absorbs a flag server at low marginal cost. Where that team differs from most is that someone’s job already covers the maintenance. **License cost dominates at your scale.** Per-seat pricing on a large engineering organization can reach numbers where a self-hosted open-source edition wins on arithmetic even after you price the operational work honestly. That calculation is worth doing properly rather than assuming either direction, and [how the pricing models actually differ](/blog/feature-flag-pricing-explained/) walks through the shapes. If none of those describe you, self-hosting is usually a cost you are choosing rather than one you are avoiding. ## 5. What the hosted model costs you Being fair about the other column matters, because a page that only lists one model’s downsides is an advertisement. You take on a vendor dependency for the control plane, so their outage becomes your inability to change flags, on their timeline, with their status page as your only visibility. You accept that flag configuration crosses a network boundary. You accept a pricing model you do not control, and the risk that it changes in a direction you dislike after you have adopted it. Migrating off is real work even where the SDK layer is portable through OpenFeature. Those are genuine. The reason the trade usually still favors hosted is that the failure modes are uncorrelated with your own, and the operational work is somebody’s full-time job rather than an interrupt in yours. ## 6. Where Featureflip sits Featureflip is a hosted service, and that is the deliberate shape of it. Every self-serve plan runs in our cloud. There is no proxy, sidecar, or cluster for you to operate, and the eight server SDKs evaluate in-process so a backend request path never waits on us. The five client SDKs work the other way round, fetching a pre-evaluated snapshot for the browser or device. On-premise deployment exists as part of an [Enterprise](/pricing/) agreement, for the residency and isolation cases in section 4. It is negotiated directly rather than offered on a self-serve plan, so Free, Team, Pro, and Business are hosted only and self-hosting is not something you can turn on yourself. Which path fits depends on what is actually driving the requirement, and the two drivers get conflated often enough to be worth separating. If the constraint is residency or isolation, where the data genuinely cannot leave infrastructure you control, that is the case the Enterprise on-premise arrangement exists for and the [pricing page](/pricing/) has the route to it. If the constraint is a license fee of zero, an open-source project serves that better than we do and Unleash is the obvious one. The [full comparison of the two approaches](/compare/featureflip-vs-unleash/) covers where its open-source edition’s limits sit and what the operational trade looks like in practice. ## The shorter version Local evaluation means a flag service outage leaves evaluation intact and freezes everything else. Your application keeps serving flags from its in-memory cache, at the same speed, with no errors. Changing a flag is the thing that stops working, and incidents are precisely when changing one matters. That reframes the self-hosting decision. Running the flag service inside your production cluster correlates its failures with your application’s, so the kill switch goes down alongside the thing it was supposed to switch off. Self-host anyway when data residency, air-gapping, or procurement demand it, and when you do, run it outside the blast radius. Otherwise let the control plane live somewhere your outages cannot reach. ## Frequently asked questions ### Are self-hosted feature flags more secure? They change the shape of the risk rather than reducing it. Self-hosting keeps flag configuration inside your perimeter, which matters under residency or air-gap rules. It also makes you responsible for patching, access control, backups, and the database, which is where most real incidents start. A hosted service concentrates that work with a provider whose job it is. The genuine security argument for self-hosting is about where data sits, and the precise answer depends on which SDK you use: server SDKs evaluate targeting attributes inside your own process, while client SDKs send the evaluation context to the service. Analytics events carry user identifiers back either way, so check what your vendor transmits rather than assuming local evaluation means nothing leaves. ### What does self-hosting a feature flag platform actually cost? The license fee on an open-source edition is zero, and everything after that is engineering time. Running the service, the database behind it, upgrades, monitoring, and on-call coverage for a service your incident response now depends on. Teams that already operate stateful internal services absorb this cheaply. Teams without a platform function tend to underestimate it, because the install is an afternoon and the maintenance is permanent. ### Can you self-host Featureflip? On-premise deployment is available as part of an Enterprise agreement, and it is negotiated directly rather than offered on the self-serve plans. The Free, Team, Pro, and Business tiers are hosted only. If free self-hosting is the requirement, an open-source project like Unleash fits that better, and our comparison page lays out the trade-offs without pretending otherwise. ### Does a feature flag outage take your application down? With local-evaluation SDKs, no. The SDK holds the flag configuration in memory and keeps evaluating from it, so calls return the same values at the same speed while the service is unreachable. Propagation stops, and new changes cannot reach your fleet until the connection recovers, which is a different problem from an outage. Architectures that call the flag service on every evaluation behave very differently, so check which one you are running. ### When should you choose a hosted feature flag service? When your flag platform’s failures should be uncorrelated with your application’s, when nobody on the team wants to own another stateful service, and when no residency or air-gap rule forbids it. That covers most teams. The cases that genuinely point at self-hosting are specific and usually obvious: regulated data that cannot leave the perimeter, a network with no outbound path, or a procurement process that blocks new vendors outright. Featureflip is a hosted feature flag platform with flat pricing, 13 official SDKs with local evaluation on the eight server ones, and streaming updates on every plan including the free one. [Start free](https://app.featureflip.io/signup) and see whether the hosted trade-off fits how your team works. --- ## Source: https://featureflip.io/contact/ # Contact # Contact Get in touch [contact@featureflip.io](mailto:contact@featureflip.io) [(770) 275-2734](tel:+17702752734) --- ## Source: https://featureflip.io/docs/ # Getting Started with Featureflip Welcome to the Featureflip documentation. Featureflip is a feature flag platform that helps you ship features safely with gradual rollouts, A/B testing, and targeting. ## Quick links - **Quickstart guides** — Get your first flag running in under 5 minutes - **What are feature flags?** — Core concepts, flag types, and lifecycle - **SDK overview** — Choose the right SDK for your platform - **Evaluation API** — REST API for evaluating flags and streaming updates - **Management API** — REST API for managing flags, environments, and targeting programmatically ## Learn the concepts - [Feature flags](/docs/concepts/feature-flags/) — Types, variations, and flag lifecycle - [Environments](/docs/concepts/environments/) — Separate configs for dev, staging, and production - [Targeting & segments](/docs/concepts/targeting/) — Serve different variations to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — Gradual percentage-based rollouts - [Plans and limits](/docs/concepts/plans-and-limits/) — What each plan includes, and what happens when you change plans ## How-to guides - [Creating flags](/docs/guides/creating-flags/) — Create and configure flags from the dashboard - [Targeting rules](/docs/guides/targeting-rules/) — Set up rules to control who sees what - [Segments](/docs/guides/segments/) — Reusable groups for targeting across flags - [Managing environments](/docs/guides/environments/) — Create environments and SDK keys - [Scheduled changes](/docs/guides/scheduled-changes/) — Queue a flag to turn on or off at a future time - [Flag expiration](/docs/guides/flag-expiration/) — Give a temporary flag the date it should come out - [Flag owners](/docs/guides/flag-owners/) — Name the person responsible for each flag - [Cleanup notices](/docs/guides/cleanup-notices/) — A daily email when a flag expires or stops being used - [Team management](/docs/guides/team-management/) — Invite members and manage roles - [Single sign-on](/docs/guides/single-sign-on/) — Sign in through Okta, Microsoft Entra ID or another OIDC provider - [Account security](/docs/guides/account-security/) — Two-factor authentication and Google or GitHub sign-in - [Webhooks](/docs/guides/webhooks/) — Call your own endpoint whenever a flag changes ## Quickstart by language ### Server-side | Language | Install | Quickstart | | --- | --- | --- | | Node.js | npm install @featureflip/node | Get started | | Python | pip install featureflip | Get started | | C# | dotnet add package Featureflip.Client | Get started | | Java | Gradle/Maven dependency | Get started | | Go | go get github.com/canopy-labs/featureflip-go/v2 | Get started | | PHP | composer require featureflip/featureflip-php | Get started | | Ruby | gem install featureflip | Get started | ### Client-side | Platform | Install | Quickstart | | --- | --- | --- | | JavaScript / TypeScript | npm install @featureflip/browser | Get started | | React | npm install @featureflip/react @featureflip/browser | Get started | | Swift (iOS/macOS) | Swift Package Manager | Get started | | Flutter | flutter pub add featureflip | Get started | | Android | Gradle dependency | Get started | --- ## Source: https://featureflip.io/docs/api/evaluate/ # Evaluate Endpoint ## Evaluate a flag Evaluates a single flag for the provided user context and returns the resolved variation. ```plaintext POST /evaluate ``` ### Request body ```json { "flagKey": "dark-mode", "context": { "userId": "user-123", "attributes": { "plan": "pro", "country": "US" } } } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | flagKey | string | Yes | The flag to evaluate | | context | object | Yes | User context for targeting | | context.userId | string | No | Unique user identifier (used for percentage rollouts) | | context.attributes | object | No | Custom attributes for targeting rules | ### Response ```json { "key": "dark-mode", "variation": "on", "value": true, "reason": "fallthrough" } ``` | Field | Type | Description | | --- | --- | --- | | key | string | The flag key that was evaluated | | variation | string | Key of the variation that was served | | value | any | The resolved value (type depends on the flag’s type) | | reason | string | Why this variation was selected (see below) | | prerequisiteKey | string | Present only when reason is prerequisite-failed: the key of the parent flag that gated this flag off | ### Evaluation reasons The `reason` is returned verbatim from the evaluator and is lower-case: | Reason | Description | | --- | --- | | rule-match: | A targeting rule matched the context. The matched rule’s ID is appended to the reason. | | fallthrough | Flag is on but no rules matched; the fallthrough variation was served. | | flag-disabled | Flag is off; the off variation was served. | | prerequisite-failed | A prerequisite flag did not serve its required variation; the off variation was served. prerequisiteKey names the parent flag. See Prerequisite Flags. | A flag key that does not exist returns `404 Not Found` rather than a `reason` value. ### Errors - `401 Unauthorized` — Invalid or missing SDK key. - `404 Not Found` — The flag key does not exist in this environment. ### Evaluation logic When you call this endpoint, the server runs the following logic: - **Flag disabled** — If `enabled` is `false`, return the `offVariation` (reason `flag-disabled`). - **Prerequisites** — Evaluate each prerequisite’s parent flag against the same context. If any parent does not serve its required variation, return the `offVariation` (reason `prerequisite-failed`, with `prerequisiteKey` set to the first failing parent). - **Rules** — Evaluate each rule in priority order. If a rule’s conditions match the context, serve according to the rule’s `serve` config (reason `rule-match:`). - **Fallthrough** — If no rules match, serve according to the `fallthrough` config (reason `fallthrough`). For `Rollout` serve configs, the user is bucketed into a variation based on a hash of the `bucketBy` attribute (defaulting to `userId`) and the configured `salt`. ## See also - [Targeting & Segments](/docs/concepts/targeting/) — how evaluation context and targeting rules work - [Prerequisite Flags](/docs/concepts/prerequisites/) — how the `prerequisite-failed` reason and `prerequisiteKey` are produced - [Rollout Strategies](/docs/concepts/rollout-strategies/) — how percentage rollouts determine variations - [Flags Endpoint](/docs/api/flags/) — fetch flag configurations and data types - [API Overview](/docs/api/overview/) — authentication, rate limiting, and all endpoints - [SDK Overview](/docs/sdks/overview/) — use an official SDK for automatic evaluation --- ## Source: https://featureflip.io/docs/api/events/ # Events Endpoint ## Record events Submits a batch of analytics events for processing. SDKs should buffer events locally and flush them periodically rather than sending one event per request. ```plaintext POST /events ``` ### Request body ```json { "events": [ { "type": "Evaluation", "flagKey": "dark-mode", "userId": "user-123", "variation": "on", "timestamp": "2026-02-16T15:30:00Z", "metadata": {} } ] } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | events | SdkEventDto[] | Yes | Array of events to record | ### SdkEventDto | Field | Type | Required | Description | | --- | --- | --- | --- | | type | string | Yes | Event type (see below) | | flagKey | string | Yes | Associated flag key | | userId | string | No | User identifier | | variation | string | No | Variation key involved | | timestamp | string | Yes | ISO 8601 timestamp (UTC) | | metadata | object | No | Arbitrary key-value pairs | ### Event types | Type | Description | | --- | --- | | Evaluation | A flag was evaluated | | Impression | A flag value was exposed to a user | | Identify | A user was identified | | Custom | A custom tracking event | ### Response Returns `202 Accepted` with an empty body. Events are queued for asynchronous processing. ### Errors - `401 Unauthorized` — Invalid or missing SDK key. ### SDK implementation notes - Buffer events in memory and flush them on a timer (e.g., every 30 seconds) and when the buffer reaches a size threshold (e.g., 100 events). - Flush remaining events when the client is closed. - If a flush fails, retain the events and retry on the next interval. ## See also - [API Overview](/docs/api/overview/) — authentication, rate limiting, and all endpoints - [Evaluate Endpoint](/docs/api/evaluate/) — evaluate flags for user contexts - [Streaming Endpoint](/docs/api/streaming/) — real-time flag updates via SSE - [SDK Overview](/docs/sdks/overview/) — official SDKs handle event buffering automatically --- ## Source: https://featureflip.io/docs/api/flags/ # Flags Endpoint ## Get all flags Fetches the complete flag configuration for the authenticated environment. SDKs typically call this once during initialization and then keep the data in memory. ```plaintext GET /flags ``` ### Response ```json { "environment": "env-abc123", "version": 42, "flags": [ { "key": "dark-mode", "version": 3, "type": "Boolean", "enabled": true, "variations": [ { "key": "on", "value": true }, { "key": "off", "value": false } ], "rules": [], "fallthrough": { "type": "Fixed", "variation": "on" }, "offVariation": "off" } ], "segments": [ { "key": "beta-testers", "version": 1, "conditions": [ { "attribute": "plan", "operator": "In", "values": ["pro", "enterprise"], "negate": false } ], "conditionLogic": "And" } ] } ``` ### Response fields | Field | Type | Description | | --- | --- | --- | | environment | string | Environment identifier for the SDK key | | version | int | Configuration version (increments on any change) | | flags | FlagDto[] | All flags in this environment | | segments | SegmentDto[] | All segments (referenced by targeting rules) | ## Get a single flag Fetches the configuration for one flag by its key. ```plaintext GET /flags/{key} ``` ### Path parameters | Parameter | Type | Description | | --- | --- | --- | | key | string | The flag key | ### Response Returns a single `FlagDto` object (see data types below). Returns `404` if the flag does not exist. ## Data types ### FlagDto | Field | Type | Description | | --- | --- | --- | | key | string | Unique flag identifier | | version | int | Flag configuration version | | type | string | "Boolean", "String", "Number", or "Json" | | enabled | bool | Whether the flag is on in this environment | | variations | VariationDto[] | All possible return values | | rules | RuleDto[] | Targeting rules, evaluated in priority order | | fallthrough | ServeConfigDto | What to serve when the flag is on and no rules match | | offVariation | string | Variation key returned when the flag is off | ### VariationDto | Field | Type | Description | | --- | --- | --- | | key | string | Variation identifier | | value | any | The value (boolean, string, number, or JSON object) | ### RuleDto | Field | Type | Description | | --- | --- | --- | | id | string | Rule identifier | | priority | int | Execution order (lower = evaluated first) | | conditions | ConditionDto[] | Conditions to evaluate | | conditionLogic | string | "And" (all must match) or "Or" (any must match) | | serve | ServeConfigDto | What to serve if the rule matches | | segmentKey | string? | Segment key if the rule targets a segment | ### ConditionDto | Field | Type | Description | | --- | --- | --- | | attribute | string | Context attribute to check (e.g., "userId", "country") | | operator | string | Comparison operator (see below) | | values | string[] | Values to compare against | | negate | bool | Invert the condition result | **Condition operators:** `Equals`, `NotEquals`, `Contains`, `NotContains`, `StartsWith`, `EndsWith`, `In`, `NotIn`, `MatchesRegex`, `GreaterThan`, `LessThan`, `GreaterThanOrEqual`, `LessThanOrEqual`, `Before`, `After` ### ServeConfigDto | Field | Type | Description | | --- | --- | --- | | type | string | "Fixed" or "Rollout" | | variation | string? | Variation key (when type is Fixed) | | bucketBy | string? | Attribute to hash for rollout bucketing | | salt | string? | Hash salt for consistent bucketing | | variations | WeightedVariationDto[]? | Weighted variations (when type is Rollout) | ### WeightedVariationDto | Field | Type | Description | | --- | --- | --- | | key | string | Variation identifier | | weight | int | Percentage weight (0—100, must sum to 100 across all entries) | ### SegmentDto | Field | Type | Description | | --- | --- | --- | | key | string | Segment identifier | | version | int | Configuration version | | conditions | ConditionDto[] | Conditions that define segment membership | | conditionLogic | string | "And" or "Or" | ## See also - [API Overview](/docs/api/overview/) — authentication, rate limiting, and all endpoints - [Evaluate Endpoint](/docs/api/evaluate/) — evaluate flags for user contexts - [Streaming Endpoint](/docs/api/streaming/) — receive real-time `flag.updated` events - [Targeting & Segments](/docs/concepts/targeting/) — understand rules, conditions, and segments - [SDK Overview](/docs/sdks/overview/) — official SDKs fetch and cache flags automatically --- ## Source: https://featureflip.io/docs/api/overview/ # Evaluation API Overview The Evaluation API is the service your SDKs communicate with to fetch flag configurations, evaluate flags, stream real-time updates, and report analytics events. Use this reference if you are building a custom SDK for a language we don’t yet support. ## Base URL ```plaintext https://eval.featureflip.io/v1/sdk ``` All endpoint paths below are relative to this base URL. ## Authentication Every request must include an **SDK key** in the `Authorization` header: ```plaintext Authorization: Bearer ``` "> SDK keys are scoped to a single environment within a project. You can create and manage them in the Featureflip dashboard under **Project Settings > Environments > SDK Keys**. An invalid or missing key returns `401 Unauthorized`: ```json { "error": "Invalid SDK key" } ``` ## Request and response format - **Content type** — Send request bodies as JSON with `Content-Type: application/json`. - **Successful responses** — Return `200 OK` or `202 Accepted` with a JSON body (or an SSE stream). - **Error responses** — Return a JSON body with an `error` field. ### Status codes | Code | Meaning | | --- | --- | | 200 | Request succeeded. | | 202 | Accepted for async processing (events). | | 401 | SDK key is missing, expired, or invalid. | | 404 | Flag not found. | | 429 | Rate limit exceeded. | ## Rate limiting The flag **evaluation** (`POST /evaluate`) and **streaming** (`GET /stream`) endpoints are not rate-limited — they are the high-frequency serving paths. The remaining endpoints (`/flags`, `/events`) apply generous limits per environment; exceeding them returns `429 Too Many Requests`. Clients should back off and retry when they receive a `429`. ## Endpoints at a glance | Method | Path | Description | | --- | --- | --- | | GET | /flags | Fetch all flag configurations | | GET | /flags/{key} | Fetch a single flag configuration | | POST | /evaluate | Evaluate a flag for a user context | | POST | /events | Record analytics events | | GET | /stream | Subscribe to real-time flag updates (SSE) | See the individual endpoint pages for request/response details and data type definitions: - [Flags](/docs/api/flags/) — fetch flag configurations and data types - [Evaluate](/docs/api/evaluate/) — evaluate a flag for a user context - [Events](/docs/api/events/) — record analytics events - [Streaming](/docs/api/streaming/) — subscribe to real-time updates via SSE ## See also - [SDK Overview](/docs/sdks/overview/) — use an official SDK instead of calling the API directly - [Environments](/docs/concepts/environments/) — understand SDK keys and environment isolation - [How to Manage Environments](/docs/guides/environments/) — create SDK keys for your environments --- ## Source: https://featureflip.io/docs/api/streaming/ # Streaming Endpoint ## Subscribe to flag updates Opens a persistent Server-Sent Events (SSE) connection that delivers flag configuration changes in real time. ```plaintext GET /stream ``` ### Headers ```plaintext Authorization: Bearer Accept: text/event-stream ``` Accept: text/event-stream"> ### Event types The stream emits two event types: #### ping Sent every 30 seconds to keep the connection alive. ```plaintext event: ping data: {"timestamp":"2026-02-16T15:30:00Z"} ``` #### flag.updated Sent when any flag or segment configuration changes in the environment. ```plaintext event: flag.updated data: {"key":"dark-mode","version":4,...} ``` The `data` payload contains the updated flag configuration in the same `FlagDto` shape described in the [Flags](/docs/api/flags/) reference. ### Errors - `401 Unauthorized` — Invalid or missing SDK key. The connection is rejected. ### SDK implementation notes - **Prefer streaming over polling.** The SSE connection delivers updates within seconds of a change, while polling introduces latency equal to the poll interval. - **Reconnect on disconnect.** If the connection drops, reconnect with exponential backoff. The `EventSource` API in browsers handles this automatically. - **Merge updates into the local cache.** When you receive a `flag.updated` event, update the corresponding flag in your in-memory store. SDKs should not need to re-fetch the full configuration. - **Handle ping silently.** Ping events exist only to keep the connection open through proxies and load balancers. No action is needed. - **Browser limitation:** The browser `EventSource` API does not support custom headers. If your SDK runs in a browser, pass the SDK key as a query parameter or use the polling strategy instead. ## See also - [API Overview](/docs/api/overview/) — authentication, rate limiting, and all endpoints - [Flags Endpoint](/docs/api/flags/) — the `FlagDto` shape used in `flag.updated` payloads - [Events Endpoint](/docs/api/events/) — record analytics events - [SDK Overview](/docs/sdks/overview/) — official SDKs handle streaming and reconnection automatically --- ## Source: https://featureflip.io/docs/concepts/environments/ # Feature Flag Environments Environments let you maintain separate flag configurations for each stage of your deployment pipeline. A flag can be enabled in development, partially rolled out in staging, and disabled in production — all at the same time. ## What are environments? An environment is an isolated context for flag configuration. When you create a project in Featureflip, it starts with three environments: - **Development** — for local development and testing - **Staging** — for pre-production validation - **Production** — for live users Each environment has its own flag states, targeting rules, and rollout percentages. Changes to one environment do not affect the others. ## Independent configuration Every flag is configured independently per environment. This means: - A flag can be **on** in Development and **off** in Production. - A flag can target beta users in Staging but use a percentage rollout in Production. - A flag’s fallthrough variation can differ between environments. This independence lets you test features safely before exposing them to real users. You promote a feature through environments as confidence grows, not by changing code. ## SDK keys Each environment has its own **SDK key**. When you initialize a Featureflip SDK in your application, you provide the SDK key for the environment the app is running in: ```js const client = FeatureflipClient.get({ sdkKey: "sdk-dev-abc123", // Development environment key }); ``` The SDK key determines which environment’s flag configurations your application receives. Use the Development key in your local environment, the Staging key in your staging deployment, and the Production key in production. Keep SDK keys out of source control. Store them in environment variables or a secrets manager. ## Creating custom environments The default three environments work for many teams, but you can create additional ones to match your workflow: - **QA** — a dedicated environment for your QA team - **Canary** — for canary deployments that receive traffic before the full fleet - **Regional** — for region-specific configurations (e.g., EU, US, APAC) When you create a new environment, Featureflip automatically generates configurations for all existing flags in that environment. Flags start in the off state, so there is no risk of unintended exposure. You can create environments from the project settings page in the Featureflip dashboard. ## Best practices **Match your deployment pipeline.** If your code goes through dev, QA, staging, and production, create an environment for each stage. This makes flag promotion predictable. **Use consistent naming.** Name environments to match what your team already calls them. If everyone says “preprod,” do not call it “staging.” **Promote flags through environments.** Enable a flag in dev first, then staging, then production. Treat flag promotion like code promotion. **Rotate SDK keys if compromised.** SDK keys are sensitive. If a key is exposed, regenerate it from the dashboard and update your application configuration. ## Next steps - [How to Manage Environments](/docs/guides/environments/) — step-by-step guide to creating environments and SDK keys - [What Are Feature Flags?](/docs/concepts/feature-flags/) — flag types, variations, and lifecycle - [SDK Overview](/docs/sdks/overview/) — choose the right SDK and learn how SDK keys are used - [Targeting & Segments](/docs/concepts/targeting/) — configure per-environment targeting rules --- ## Source: https://featureflip.io/docs/concepts/feature-flags/ # What Are Feature Flags? Feature flags let you change what your application does at runtime, without deploying new code. They act as decision points in your codebase that you control from a dashboard or API. ## What are feature flags? A feature flag is a conditional wrapper around functionality. In code, it looks something like this: ```js if (featureflip.boolVariation("new-checkout", user, false)) { showNewCheckout(); } else { showOldCheckout(); } ``` The flag’s value is determined by configuration on the Featureflip server, not by your application code. You can change which variation is served to users instantly, without redeploying. Feature flags are sometimes called feature toggles, feature switches, or feature gates. They all refer to the same concept. ## Why use feature flags? **Safe rollouts.** Release a feature to 5% of users first. If metrics look good, increase to 25%, then 50%, then 100%. If something breaks, roll back to 0% instantly. **Kill switches.** Wrap risky or resource-heavy features in a flag. If a feature causes problems in production, disable it in seconds rather than reverting a deployment. **Trunk-based development.** Merge incomplete features into your main branch behind a flag. The code ships but the feature stays hidden until it is ready. No more long-lived feature branches. See [merging unfinished work into trunk](/blog/feature-flags-trunk-based-development/) for the CI and database-migration side of working this way. **A/B testing.** Serve different variations to different groups of users and measure which performs better. **Targeted access.** Give beta users, internal teams, or enterprise customers early access to features while keeping them hidden from everyone else. ## Flag types in Featureflip Featureflip supports four flag types: | Type | Values | Example use | | --- | --- | --- | | Boolean | true or false | Toggle a feature on or off | | String | Any text value | Select a theme: "blue", "green", "red" | | Number | Any numeric value | Set a rate limit: 100, 500, 1000 | | JSON | Structured data | Pass configuration: {"maxItems": 10, "layout": "grid"} | Boolean flags are the most common. Use string, number, or JSON flags when you need to control more than just on/off behavior. ## Variations Every flag has a set of **variations** — the possible values it can return. A boolean flag always has exactly two variations: `true` and `false`. Other flag types can have as many variations as you need. For example, a string flag called `checkout-button-color` might have three variations: - `"blue"` (the current design) - `"green"` (a new design being tested) - `"red"` (another candidate) Targeting rules and rollout strategies determine which variation each user receives. See [Targeting & Segments](/docs/concepts/targeting/) and [Rollout Strategies](/docs/concepts/rollout-strategies/) for details. ## Flag lifecycle A typical flag goes through these stages: - **Create** — Define the flag with its type and variations in the Featureflip dashboard or via the API. - **Configure targeting** — Set up rules to control who gets which variation. - **Enable in development** — Turn the flag on in your dev environment and verify behavior. - **Test** — Validate in staging or QA with broader access. - **Enable in production** — Roll out to real users, often starting with a small percentage. - **Monitor** — Watch metrics and error rates to confirm the feature is working. - **Archive** — Once the feature is fully rolled out and stable, remove the flag from your code. - **Delete** — Clean up the flag from Featureflip. Flags that live too long become technical debt. Plan to remove them once they have served their purpose — Featureflip [detects stale and dead flags automatically](/docs/concepts/stale-flags/) from evaluation traffic, so finished flags surface on their own. For a step-by-step walkthrough, see [How to Create Feature Flags](/docs/guides/creating-flags/) . ## Off variation vs. default variation These two concepts are easy to confuse: **Off variation.** When a flag is **disabled** (toggled off), the off variation is served to all users. For boolean flags, this is `false`. No targeting rules are evaluated. **Default (fallthrough) variation.** When a flag is **enabled** but a user does not match any targeting rules, the fallthrough variation is served. This is the “catch-all” for users who are not specifically targeted. Here is the evaluation flow: - Is the flag enabled? **No** — serve the off variation. Stop. - Is the flag enabled? **Yes** — evaluate targeting rules top to bottom. - Does a rule match? **Yes** — serve that rule’s variation. Stop. - No rules matched — serve the fallthrough (default) variation. This distinction gives you precise control: you can target specific users with rules, define a default experience for everyone else, and still have a quick way to shut the feature off entirely. ## Next steps - [How to Create Feature Flags](/docs/guides/creating-flags/) — step-by-step guide to creating flags in the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Prerequisite Flags](/docs/concepts/prerequisites/) — make one flag depend on another flag’s variation - [Reliability & Resilience](/docs/concepts/reliability/) — safe defaults, caching, and what happens when the service is down - [SDK Overview](/docs/sdks/overview/) — choose the right SDK for your platform and get started ## Frequently Asked Questions ### What is the difference between feature flags and feature toggles? Feature flags and feature toggles are the same concept — different names for the ability to turn features on or off without deploying new code. The term “feature flag” is more widely used today, popularized by continuous delivery practitioners, while “feature toggle” comes from the older trunk-based development community. You may also hear “feature switch” or “feature gate” — all refer to the same pattern. The distinction that actually matters is not the name but the type: release toggles (short-lived, for deployment decoupling), experiment toggles (for A/B testing), ops toggles (kill switches for risky behavior), and permission toggles for controlling user-tier access. Featureflip supports all of these use cases through a single flag abstraction with boolean, string, number, and JSON flag types. Whether your team calls them flags or toggles, the workflow is the same: wrap functionality in a conditional, configure the value remotely, and change behavior without touching code or redeploying. ### When should I use feature flags? Use feature flags when you want to decouple deployment from release — ship code to production before it is visible to users. This is the foundation of trunk-based development: merge incomplete features behind a flag so your team avoids long-lived branches. Use flags for gradual rollouts, starting at 5% of users and expanding as metrics confirm stability. Use them as kill switches for any feature that could degrade performance or trigger errors — you can disable it in seconds, no deployment required. Run A/B experiments by serving different variations to different user groups and measuring which performs better. Grant early access to beta users, internal teams, or enterprise customers while keeping features hidden from the general population. If you are shipping software to production more than once a week, feature flags should be a standard part of your workflow. Featureflip supports all of these patterns out of the box. ### Are feature flags a security risk? Feature flags themselves are not a security risk, but there are important principles to follow. Never use feature flags as a substitute for proper server-side authorization. A flag controls whether a feature is visible or active — it does not restrict access at the data or API level. Sensitive operations should always have permission checks independent of any flag state. On the configuration side, treat SDK keys like API keys: store them in environment variables or a secrets manager, never in source code. Featureflip uses separate SDK keys per environment, limiting the blast radius if a key is exposed. Flag configurations are not user-facing secrets, but you should still rotate keys if one is compromised. The bigger risk with feature flags is operational: flags that are never cleaned up become invisible branches in your code. Audit and archive flags regularly to keep your codebase understandable and your attack surface small. ### Do feature flags slow down my application? With Featureflip, flag evaluation is designed for minimal latency. Server-side SDKs — for Node, Python, Go, Java, C#, PHP, and Ruby — download the full flag configuration once at startup and cache it in memory. Flag checks are pure in-memory lookups that typically complete in under one millisecond, adding no network round-trips to individual requests. Configuration updates are pushed to SDKs in real time via Server-Sent Events, so you get fresh values without polling. Client-side SDKs for browsers, React, Flutter, Android, and Swift receive pre-evaluated flag results tailored to the current user, keeping the payload small. The evaluation infrastructure is purpose-built for low latency and high throughput, handling millions of flag checks without adding measurable overhead to your application. The practical performance impact of adding feature flags to your application is negligible — the evaluation overhead is far smaller than a typical database query or API call. --- ## Source: https://featureflip.io/docs/concepts/plans-and-limits/ # Plans and Limits: What Each Featureflip Plan Includes Featureflip has four plans you can sign up for yourself (Free, Team, Pro and Business) and an Enterprise plan arranged with our team. This page lists what each one allows and what changes when you move between them. Prices are on the [pricing page](/pricing/) . ## Limits by plan | | Free | Team | Pro | Business | Enterprise | | --- | --- | --- | --- | --- | --- | | Team members | 3 | 5 | 10 | 25 | Unlimited | | Projects | 1 | 3 | 10 | 25 | Unlimited | | Environments per project | 2 | 3 | 5 | Unlimited | Unlimited | | Flags per project | 10 | Unlimited | Unlimited | Unlimited | Unlimited | | Webhook endpoints | 1 | 3 | 3 | Unlimited | Unlimited | | Audit log retention | 7 days | 30 days | 30 days | 90 days | 365 days | ## Features that depend on the plan | Feature | Available on | | --- | --- | | Scheduled changes | Team, Pro, Business, Enterprise | | Cleanup notice emails | Team, Pro, Business, Enterprise | | Flag owners | Pro, Business, Enterprise | | Single sign-on through your own OIDC identity provider, with Require single sign-on | Business, Enterprise | | SAML and SCIM | Enterprise | Everything else is on every plan, Free included: targeting rules, segments, percentage rollouts, prerequisites, all 13 SDKs, the [Management API](/docs/management-api/overview/) , the [MCP server](/docs/integrations/mcp/) , the [Terraform provider](/docs/integrations/terraform/) , the [audit log](/docs/guides/audit-log/) , [flag expiration dates](/docs/guides/flag-expiration/) , [stale flag detection](/docs/concepts/stale-flags/) , the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) , two-factor authentication, and sign-in with Google or GitHub. ## How the limits are counted Four of them need a word of explanation. - **Team members include pending invitations.** An invite you’ve sent holds a seat until it’s accepted, revoked or expires. Revoke stale invites to get the seat back. - **Environments are counted per project.** Each project has its own allowance, so on Team all three of your projects can have three environments each. A new project starts with Development, Staging and Production, trimmed to fit the plan. On Free that leaves Development and Production, and the project has no room for a third. - **Flags per project counts active flags only.** Archiving a flag frees its place, and the archived flag keeps its key. Restoring it takes a place again, so on Free a project that already has 10 active flags can’t restore another until you archive or delete one. - **Webhook endpoints** are the subscriptions in Organization Settings, whichever events and projects each one covers. When you reach a limit, Featureflip refuses to create the next item and tells you which limit you hit, for example “Project limit of 3 reached for Team plan. Upgrade to create more projects.” ## Upgrading A higher plan’s limits and features apply as soon as the upgrade goes through. Plans are managed in the **Billing** section of Organization Settings, and only the organization’s Owner can open it. Enterprise isn’t sold there. For that, use the address on the [pricing page](/pricing/) . ## Downgrading or cancelling Cancelling a paid subscription moves your organization to Free at the end of the billing period you’ve paid for. What follows applies whenever the organization lands on a plan with less room, whether you cancelled or moved to a smaller paid plan. **Your configuration stays.** Projects, environments, flags, members and webhook subscriptions above the new plan’s limits are left in place and keep working, and webhooks over the limit go on delivering. You just can’t add more. Until you’re back under a limit, creating another item of that kind is refused. **Scheduled changes (below Team).** Pending changes still run, and they stay listed on the flag’s page, where you can cancel any you no longer want. You can’t schedule new ones. **Cleanup notices (below Team).** The emails stop. Expired and dead flags keep their markers in the flags list. Upgrade again and notices resume with flags that qualify from that day on, so whatever piled up in the meantime never arrives as one batch. **Flag owners (below Pro).** Owners stay stored but are hidden in the dashboard, and you can’t name new ones. On Team, cleanup notices go to your Owners and Admins rather than to each flag’s owner. Upgrade again and the stored owners reappear. [Flag owners](/docs/guides/flag-owners/#plan-requirements) has the details. **Single sign-on (below Business).** Sign-in through your identity provider stops, **Require single sign-on** is no longer enforced, and your connection settings are kept for when you come back. Anyone who only ever signed in through SSO needs to set a password first, and the [single sign-on guide](/docs/guides/single-sign-on/#if-your-organization-moves-below-business) shows how to do that from the sign-in page. **Audit log.** Retention follows the new plan. Entries older than its window are removed in the next daily cleanup, so on Free anything older than 7 days goes. If you need that history, review or copy what you need from the [audit log](/docs/guides/audit-log/) before your plan changes. ## Prices for existing subscribers If your subscription started before the current prices, it keeps the price you signed up at. It moves to the current price when you change plans or switch between monthly and annual billing. ## Deleting an organization Only the Owner can delete an organization, and only while it’s on Free. On a paid plan, cancel the subscription first and wait for the billing period to run out, at which point the organization drops to Free and the delete goes through. ## Next steps - [Pricing](/pricing/) : current prices, monthly and annual - [Team Management](/docs/guides/team-management/) : invite members and manage roles - [Flag Owners](/docs/guides/flag-owners/) : the Pro feature for naming who is responsible for each flag - [Single Sign-On](/docs/guides/single-sign-on/) : connect your identity provider on Business and Enterprise --- ## Source: https://featureflip.io/docs/concepts/prerequisites/ # Prerequisite Flags A prerequisite flag is a dependency between two flags: a **child** flag stays off until a **parent** flag serves a specific variation. The dependency is enforced inside the evaluator, before any of the child’s targeting rules run, so the gating logic lives in Featureflip rather than in your application code. This replaces the `if (parent) { check child }` pattern that flag-heavy codebases tend to accumulate. Instead of duplicating a parent’s targeting across every flag that should follow it, each dependent flag declares the parent and the variation it requires, and the platform keeps them in sync. ## How prerequisites are evaluated Each flag carries an ordered list of prerequisites alongside its targeting rules and fallthrough. When the child flag is evaluated: - The evaluator walks the prerequisites in order. - For each one, it evaluates the **parent** flag against the same user context and compares the served variation to the variation the prerequisite requires. - If any prerequisite’s parent does not serve the required variation, the child short-circuits to its **off variation** and stops — the targeting rules never run. - Only when **every** prerequisite passes does the evaluator continue to the child’s targeting rules and fallthrough. Because the check happens before targeting, a prerequisite is an AND gate across all of a flag’s dependencies: every parent must serve its required variation for the child to be considered “on” at all. The same algorithm runs server-side in the Evaluation API and locally in each server-side SDK, so a flag check returns the same answer wherever it runs. For the exact reason strings the HTTP endpoint returns, see the [Evaluate endpoint reference](/docs/api/evaluate/) . ## Variation match, not just on/off A prerequisite names a specific variation the parent must serve, not merely “the parent is on.” That makes prerequisites work for multivariate flags. A plan-tier flag with variations `free`, `pro`, and `enterprise` can act as the parent for several [entitlement flags](/use-cases/entitlements/) , where one entitlement requires `enterprise` and another requires `pro`. When a customer’s plan changes and the parent starts serving a different variation, every entitlement that required the old value falls back to off in the same evaluation. Featureflip prerequisites support **equals** matching only. To express “parent must *not* serve variation X,” invert the requirement by naming the variation the parent *should* serve instead. ## What you see when a prerequisite gates a flag off When a prerequisite is not met, the dependent flag serves its off variation with: - **reason** set to `prerequisite-failed` (the server and HTTP Evaluation API) or `PrerequisiteFailed` (client-side SDK evaluation). - **prerequisiteKey** set to the key of the first parent flag that failed its check. Client-side SDKs receive this only when that parent is itself marked client-side visible, so a browser is never handed the key of a flag its SDK key was not granted. The `reason` is the same either way, and server-side SDKs always see the key. That `prerequisiteKey` is what makes a misfiring gate debuggable from a single evaluation detail: the result names the parent responsible, rather than leaving you to inspect a child whose own rules all evaluated cleanly. ```ts const detail = client.variationDetail('beta-checkout', context, false); if (detail.reason === 'PrerequisiteFailed') { console.log(`beta-checkout gated off by ${detail.prerequisiteKey}`); } ``` ## Guardrails A few invariants are enforced for you, so a prerequisite graph cannot get into a broken state: | Guardrail | Behavior | | --- | --- | | Cycle detection | A prerequisite that would create a loop (for example flag-a depends on flag-b which depends back on flag-a) is rejected when you save it, with the offending chain returned in the error. | | Depth cap | Prerequisite chains are capped at a depth of 10 as a defense-in-depth safety net. Realistic graphs are usually one to three levels deep. | | Delete protection | You cannot delete a flag that other flags use as a prerequisite. The API returns 400 FLAG_HAS_DEPENDENTS with the dependent flag keys so you know what to unwire first. | | Variation-removal protection | You cannot remove a variation that another flag’s prerequisite expects (VARIATION_HAS_DEPENDENTS). | | Archive / restore | A flag cannot be archived while active flags depend on it, and a flag cannot be restored while its own prerequisites are still archived. Archiving an entire dependency chain in one bulk selection works. | ## Prerequisites vs. targeting rules These solve different problems and run at different stages: - A **targeting rule** decides *which* variation to serve, based on context attributes such as country, plan, or user ID. - A **prerequisite** decides *whether the flag may serve anything other than off*, based on another flag’s result. Prerequisites are checked first. If any prerequisite fails, the targeting rules never get a chance to fire. ## Next steps - [How to Set Prerequisite Flags](/docs/guides/prerequisites/) — add and manage prerequisites in the dashboard - [What Are Feature Flags?](/docs/concepts/feature-flags/) — flag types, variations, and the flag lifecycle - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluate Endpoint](/docs/api/evaluate/) — the reason strings and `prerequisiteKey` returned by the API ## Frequently Asked Questions ### What is a prerequisite flag? A prerequisite flag is a dependency between two flags: a child flag stays off until a parent flag serves a specific variation. Featureflip enforces the dependency inside the evaluator, before the child’s targeting rules run, so the gating logic lives in the platform instead of your application code. Prerequisites are variation-match, so they work for multivariate flags such as a plan-tier flag, not only on/off booleans. ### What is the difference between a prerequisite and a targeting rule? A targeting rule decides which variation to serve based on context attributes such as country, plan, or user ID. A prerequisite decides whether the flag is allowed to serve anything other than its off variation, based on another flag’s result. Prerequisites are checked first; if any prerequisite fails, the targeting rules never run and the child serves its off variation. ### What happens to a dependent flag when its prerequisite is not met? The dependent flag serves its off variation, and the evaluation reason is set to `prerequisite-failed` (`PrerequisiteFailed` in client-side SDK evaluation). The result’s `prerequisiteKey` field names the parent flag that gated it off, so you can see exactly which dependency failed. Client-side SDKs receive prerequisiteKey only when the parent flag is itself marked client-side visible, so a browser is never handed the key of a flag it was not granted. Disabling a parent flag, or changing its targeting so it no longer serves the required variation, gates every dependent flag off in the same evaluation cycle. ### Can prerequisite flags create circular dependencies? No. Cycles are detected when you save a prerequisite and rejected, with the offending chain returned in the error. As a defense-in-depth safety net the evaluator also caps prerequisite chains at a depth of 10, but in practice most flag graphs are only one to three levels deep. You also cannot delete a flag, or remove a variation, that another flag depends on until the dependent references are removed first. --- ## Source: https://featureflip.io/docs/concepts/reliability/ # Feature Flag Reliability & Resilience Feature flags sit in the request path of almost everything you ship, so the question that matters before any feature comparison is operational: what happens when Featureflip is slow, unreachable, or misconfigured? Adding flags should not add a new way for your application to fall over. This page collects, in one place, how Featureflip behaves under failure — local evaluation, cached values, safe defaults, and graceful streaming fallback. The short version: a Featureflip outage does not take your application down, and a missing or un-resolvable flag falls back to a value you control. ## What happens when Featureflip is unavailable Featureflip SDKs do **not** make a network call on every flag check. They fetch the flag configuration once, hold it, and evaluate against that local copy. That single design choice is what makes flag checks both fast and resilient: - **Server-side SDKs** (Node.js, Python, Go, Java, C#, Ruby) download the full flag configuration at startup and keep it in an in-memory store. A flag check is an in-memory lookup that typically completes in well under a millisecond. If the Evaluation API goes down *after* startup, evaluation is unaffected — the SDK keeps serving from its in-memory copy and reconnects in the background. - **The PHP SDK** is cache-first by design, because PHP’s request-per-process model makes a long-lived in-memory client impractical. It writes the configuration through a PSR-16 cache (Redis, Memcached, APCu, or filesystem) and serves subsequent requests from cache without a network round trip. - **Client-side SDKs** (Browser/JS, React, Android, Swift, Flutter) receive *pre-evaluated* values and keep them in an in-memory snapshot. The Android and Swift SDKs additionally persist the last successful snapshot to disk, so flags still resolve when the app cold-starts in airplane mode or on a flaky network — they serve the values from the last successful sync and retry quietly in the background. If a value cannot be resolved at all, the SDK returns the default you supplied (see Safe defaults below). Nothing in the evaluation path waits on the network. ## Safe defaults: the fallback you control Every variation method takes an explicit **default value** as a required argument. That default is returned whenever the flag is missing, the value has an unexpected type, the client has not finished loading yet, or any error occurs during evaluation: ```ts // Node.js — the third argument is the default const checkout = client.boolVariation('new-checkout', { user_id: 'u-123' }, false); ``` ```python # Python — variation() never raises; it returns the default on any error checkout = client.variation("new-checkout", {"user_id": "u-123"}, default=False) ``` Because the safe value lives at the call site, your fallback behavior is a local decision, not a remote one. Evaluation is built to stay out of your error path: the Python and PHP SDKs are documented to **never throw** from a variation call, and the other SDKs return the default instead of surfacing an exception. Choose each default as the value you want users to get if Featureflip could tell you nothing at all — usually the current, safe code path. For the difference between a flag’s *off* variation and its *fallthrough* (default) variation inside the evaluator, see [What Are Feature Flags?](/docs/concepts/feature-flags/#off-variation-vs-default-variation) . ## Caching and last-known-good values | SDK group | Where flag data lives | Survives an outage by | | --- | --- | --- | | Node.js, Python, Go, Java, C#, Ruby | In-memory configuration loaded at startup | Evaluating locally with no per-request network call | | PHP | PSR-16 cache (Redis, Memcached, APCu, filesystem) | Reading the configuration from cache between requests | | Browser / JS, React | In-memory snapshot, updated by streamed deltas | Serving the last values received | | Android, Swift | In-memory snapshot plus on-disk cache | Resolving to the last successful sync, even on a cold start with no network | Server-side SDKs receive streamed *delta* updates and merge them into the in-memory store rather than re-fetching the whole configuration, so a brief disconnect costs you nothing once it recovers. Client SDKs work the same way for the values they hold. The PHP SDK is the exception throughout this section: PHP has no process that outlives a request, so it polls and re-fetches the whole configuration rather than streaming deltas. ## Real-time updates and graceful degradation By default, SDKs other than PHP subscribe to configuration changes over **Server-Sent Events (SSE)**, so a rollout you adjust in the dashboard takes effect across your fleet within seconds — no redeploy, no restart. The connection is designed to degrade gracefully rather than fail: - If the stream drops, the SDK **reconnects with exponential backoff**. - After five failed attempts in a row, the JavaScript, Node.js, Python, Go, Java, Ruby, Swift, Android and Flutter SDKs **fall back to polling** and keep serving the last known values the entire time. They poll on the poll interval, 30 seconds by default, except Go, which polls every 3 seconds while it is in fallback. JavaScript and Node.js let you change the attempt count with `maxStreamRetries`, and Ruby with `max_stream_retries`. - The C# SDK keeps retrying the stream instead, with one corrective poll after each failed attempt, and the browser SDK retries for as long as the page is open. - A `ping` keepalive every 30 seconds holds the connection open through proxies and load balancers. - Python and Go keep retrying the stream while they poll, and go back to streaming as soon as it reconnects. In the other SDKs that fall back, polling lasts until the client restarts, so restart once whatever blocked the stream is fixed. You can also disable streaming and run polling-only if your environment forbids long-lived connections (`streaming: false`). Either way, evaluation never blocks on the network — it always reads from the local store. ## Initialization behavior Because evaluation reads from a local copy, the only moment the network matters is the **initial load**. SDKs handle a slow or failing first fetch deliberately: - **Load in the background (default).** Most SDKs return a client immediately and load flags in the background. Until the first load completes, evaluations return your defaults. Call `waitForInitialization()` (or the `create()` helper) if you would rather block until flags are ready. - **Start offline.** The C# SDK exposes `StartOffline`, which lets the client start even if the first fetch fails — it serves defaults and retries in the background. - **Fail fast.** The Python SDK initializes synchronously and raises `InitializationError` if the first fetch fails or times out, so you can decide at startup whether to proceed. After initialization, `variation()` still never raises. The initial-load timeout defaults to **10 seconds** (`initTimeout`); server SDKs that expose them default to a **5-second** connect timeout and a **10-second** read timeout. All are configurable. Each SDK exposes an `isInitialized` flag so you can gate readiness checks or health probes. ## Misconfiguration and guardrails Resilience is not only about outages. Featureflip is built so that a configuration mistake fails toward safety, and so that mistakes are reviewable after the fact: - **A disabled flag serves its off variation to everyone** with no targeting evaluated — the fastest, most predictable way to shut a feature off. This is the basis of the [kill switch](/docs/concepts/feature-flags/#why-use-feature-flags) pattern. - **Separate SDK keys per environment** limit the blast radius of a leaked key, and client keys only ever expose flags marked *Client-side visible*. See [client-side vs server-side](/docs/sdks/overview/#client-side-vs-server-side) . - **Flags are not an authorization layer.** A flag controls whether a feature is visible or active; it does not gate data or API access. Keep server-side permission checks independent of flag state. See [Are feature flags a security risk?](/docs/concepts/feature-flags/#are-feature-flags-a-security-risk) - **Every change is recorded.** Who toggled a flag, changed a rule, or rotated a key, with before and after values, is captured automatically, so a misconfiguration has an answer to “what changed, and when?” See the [Audit Log](/docs/guides/audit-log/) . - **Finished flags surface on their own.** [Stale flag detection](/docs/concepts/stale-flags/) flags conditionals that no longer make a real decision, so dead branches do not linger as hidden risk. ## Next steps - [SDK Overview](/docs/sdks/overview/) — server-side vs client-side, and SDK key security - [Audit Log](/docs/guides/audit-log/) — who changed what, and when - [Streaming Endpoint](/docs/api/streaming/) — the SSE contract and reconnection notes - [What Are Feature Flags?](/docs/concepts/feature-flags/) — off vs default variations and the evaluation flow ## Frequently Asked Questions ### What happens if the Featureflip service is unavailable? Your application keeps serving flags. Featureflip SDKs do not call the service on every flag check. Server-side SDKs download the flag configuration once at startup and evaluate locally from an in-memory copy, so an outage after startup has no effect on evaluation. Client-side SDKs hold the last set of values they received, and the Android and Swift SDKs persist the last successful snapshot to disk so flags still resolve on a cold start with no network. If a value genuinely cannot be resolved, the SDK returns the default you passed to the evaluation call. The connection retries in the background and recovers on its own once the service is reachable again. ### Does Featureflip cache feature flag values? Yes. Server-side SDKs (Node.js, Python, Go, Java, C#, Ruby) keep the full flag configuration in an in-memory store and evaluate against it with no per-request network call. The PHP SDK is cache-first: it writes the configuration through a PSR-16 cache (Redis, Memcached, APCu, or filesystem) and reads from it between requests. Client-side SDKs keep an in-memory snapshot, and the Android and Swift SDKs also persist it to disk so the last known values are available offline and on the next cold start. ### What value does a feature flag return if evaluation fails? Every variation method takes an explicit default value, and that default is what you get if the flag is missing, the value has an unexpected type, the client has not finished loading, or any error occurs. Evaluation is designed not to throw from your hot path: the Python and PHP SDKs are documented to never raise from a variation call, and the other SDKs return the default rather than surfacing an exception. Because the default is required at every call site, the safe fallback behavior is whatever you chose locally, not a remote decision. ### Can I use Featureflip server-side only? Yes. Server-side SDKs use a server SDK key that stays on your infrastructure and is never exposed to users, and they can see every flag in the project. You do not need to ship any client-side SDK or expose flags to the browser. If you do want client-side evaluation, client SDK keys are public by design and only return flags explicitly marked Client-side visible, so your targeting rules never reach the browser. Server keys and client keys are separate and managed per environment. ### How does Featureflip handle real-time updates if the streaming connection drops? SDKs subscribe to flag changes over Server-Sent Events and apply updates within seconds. If the stream drops, the SDK reconnects with exponential backoff. After five failed attempts in a row, most SDKs fall back to polling on the poll interval (30 seconds by default, every 3 seconds in Go) and keep serving the last known values the whole time. Python and Go return to streaming once the connection recovers, and the rest stay on polling until the client restarts. Throughout, evaluation continues against the in-memory configuration, so a dropped connection never blocks a flag check. --- ## Source: https://featureflip.io/docs/concepts/rollout-strategies/ # Rollout Strategies & Percentage Rollouts Rollout strategies determine how variations are distributed to users. Featureflip supports two strategies: fixed serving and percentage rollout. You can use them independently or combine them with targeting rules. ## Fixed serving With fixed serving, every user who matches a rule (or reaches the fallthrough) gets the same variation. There is no randomness or splitting. This is the default strategy and the simplest to reason about: - Rule matches “enterprise” users — all enterprise users get `true` - Fallthrough — everyone else gets `false` Fixed serving is the right choice when you want a binary decision: a group of users either gets the feature or they do not. ## Percentage rollout With percentage rollout, traffic is split across variations by percentage. For example: - `true` — 20% of users - `false` — 80% of users This is useful for gradual rollouts (test with a small group before going wide) and A/B testing (compare two or more variations against each other). You can apply a percentage rollout to the fallthrough or to individual targeting rules. ## How percentage rollout works Featureflip uses a deterministic hashing algorithm to assign users to buckets: - The user’s ID and the flag key are combined and hashed. - The hash produces a number between 0 and 100. - That number determines which variation the user receives based on the configured percentages. Because the hash is deterministic, the **same user always gets the same variation** for the same flag. A user will not flip between variations on page refresh, across sessions, or between API calls. This consistency is important for a good user experience. Different flags produce different hashes for the same user, so a user who is in the 20% group for one flag is not necessarily in the 20% group for another. Note For percentage rollouts to work correctly, include a `user_id` (or similar stable identifier) in the evaluation context — it is what places each user in a consistent bucket. Anonymous contexts (no `user_id`) cannot be bucketed: server-side SDKs evaluating locally serve the **first (control) variation**, while the hosted evaluation endpoint spreads anonymous traffic across variations by weight (without per-user stickiness). For a sticky anonymous experience, generate and persist a stable key yourself and pass it as the `user_id`. ## Gradual rollout pattern The most common use of percentage rollout is a gradual feature release: - **5%** — Release to a small group. Monitor error rates and key metrics. - **25%** — Expand if metrics look healthy. Watch for edge cases. - **50%** — Half of users now have the feature. Continue monitoring. - **100%** — Full rollout. The feature is live for everyone. At any step, if you see problems, reduce the percentage back down or set it to 0%. There is no deployment involved — the change takes effect within seconds. Once a feature is at 100% and stable, remove the flag from your code and delete it from Featureflip. Stale flags add complexity. For more on the rollout ladder, what to monitor at each step, common pitfalls, and when *not* to reach for a percentage gate, see [Progressive rollouts with feature flags](/use-cases/progressive-rollouts/) . ## Combining strategies Targeting rules and rollout strategies work together. A common pattern: - **Rule 1:** `email` ends with `"@yourcompany.com"` — serve `true` (fixed). Your internal team always sees the feature for dogfooding. - **Rule 2:** Segment `"Beta Users"` — serve `true` (fixed). Beta testers always see the feature. - **Fallthrough:** 10% get `true`, 90% get `false` (percentage rollout). A small slice of general users also see the feature. Rules are evaluated first, top to bottom. Users who match a rule get that rule’s variation and skip the fallthrough. Users who do not match any rule fall through to the percentage rollout. This lets you guarantee access for specific groups while gradually exposing the feature to the broader user base. ## Next steps - [Progressive rollouts with feature flags](/use-cases/progressive-rollouts/) — the rollout ladder, monitoring at each step, and common pitfalls - [Targeting & Segments](/docs/concepts/targeting/) — attribute-based targeting and reusable segments - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — step-by-step guide to configuring rules and rollouts - [How to Use Segments](/docs/guides/segments/) — create reusable user groups - [Evaluation API](/docs/api/evaluate/) — see how rollouts are resolved via the REST API --- ## Source: https://featureflip.io/docs/concepts/stale-flags/ # Stale Flag Detection Featureflip automatically classifies every flag in every environment as **Active**, **Stale**, or **Dead** based on the evaluation traffic it actually receives. There is nothing to install or configure: detection runs continuously, and flags that have stopped making real decisions surface in the dashboard with the reasons attached. ## Why stale flags matter Most feature flags are meant to be temporary. A rollout flag should be deleted once the feature is at 100%; an experiment flag should be deleted once the experiment has a winner. In practice, deletion gets deferred, and every leftover flag is a conditional branch your team still has to read, test, and debug around. The hard part of cleanup has always been the first step: knowing which flags are actually done. Staleness detection answers that question for you, from evaluation data rather than guesswork. ## The three statuses | Status | Meaning | | --- | --- | | Active | The flag is making real decisions: traffic is flowing and more than one variation is in play (or it is too early to tell). | | Stale | A staleness signal has persisted for the last 30 days. The flag is probably finished, and the team likely still has context to remove it cheaply. | | Dead | The same signal has persisted for the last 90 days. The flag has not made a meaningful decision in three months and is a clear removal candidate. | Statuses are recomputed hourly from evaluation analytics, so a flag that becomes active again — for example, you re-enable it or change its targeting — moves back to Active on the next pass without any manual reset. ## What detection looks for Each flag-environment is checked for four independent signals. A flag can carry more than one. | Signal | Fires when | What it usually means | | --- | --- | --- | | Fully rolled out | The flag is enabled, serving a fixed single variation, and at least 99.9% of evaluations return that variation. | The rollout is complete. Ship the winning code path unconditionally and remove the flag. | | Rolled back | The flag is disabled and at most one variation has been served in the window. | The feature was turned off and abandoned. Decide whether it is coming back; if not, remove it. | | No traffic | The flag is enabled but received zero evaluations in the window, despite having traffic before. | The code path that evaluates the flag no longer runs — often the reference was already deleted. | | Never used | The flag was created 30+ days ago (90+ for Dead) and has never been evaluated in this environment. | The flag was created speculatively or for an environment it is not wired into. | ## Flags with an expiration date A flag can also carry a declared [expiration date](/docs/guides/flag-expiration/) , the day you expect it to be removed. Once that day passes, detection adds a fifth reason, `PastExpiry`, always at the Stale tier. It doesn’t wait the way the four signals above do. Those need to hold steady for 30 or 90 days before they mark a flag Stale or Dead, while `PastExpiry` applies the moment the date passes, since a stated date carries no ambiguity to wait out. It also never drives the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) on its own: a passed date is declared intent, not evidence the flag has stopped mattering. ## Per-environment detail and roll-up Detection runs per environment, because flags are usually finished in some environments before others — fully rolled out in production while a forgotten staging config still serves both variations, for example. - The **flag list** shows one badge per flag: the worst status across all of its environments, with the combined reasons. - The **flag detail Environments tab** shows each environment’s own status and reasons, so you can see exactly where a flag is still live before removing it. - The flag list can be **filtered by staleness status** to produce a ready-made cleanup queue. ## What staleness detection is not **It does not scan your codebase.** Featureflip sees how flags behave in running systems, not where they are referenced in your repository. Before deleting a flag, find its references with a code search and remove them first — the [feature flag cleanup guide](/blog/feature-flag-cleanup/) covers a practical workflow, including the two-PR removal pattern. **It does not act on flags for you.** Detection is a signal, not an enforcement mechanism. Featureflip never archives, disables, or deletes a flag on its own, and the statuses exist to make your cleanup decisions cheap and well-informed. If you want the acting done too, the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) reads these same statuses and opens a pull request removing the flag’s code, which you review and merge. **Permanent flags will show up — by design.** A kill switch that has served `true` to everyone for a year is, by traffic analysis, indistinguishable from a finished rollout. Treat the badge as a review prompt rather than an order: if a flag is intentionally permanent, that is a fine answer, and the badge simply tells you it is worth confirming the intent still holds. ## Next steps - [What Are Feature Flags?](/docs/concepts/feature-flags/) — flag types, variations, and the flag lifecycle - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production - [Flag Cleanup Action](/docs/integrations/flag-cleanup-action/) — remove dead flag code by pull request, and archive the flag when it merges - [Cleanup Notices](/docs/guides/cleanup-notices/) — a daily email when a flag goes Dead in every environment or passes its expiry date - [Feature flag cleanup](/blog/feature-flag-cleanup/) — the detect, triage, remove, prevent loop, with code-search tooling - [Rollout Strategies](/docs/concepts/rollout-strategies/) — percentage rollouts and fixed serving ## Frequently Asked Questions ### What is a stale feature flag? A stale feature flag is a flag that no longer makes a live decision in your application: it serves the same variation to everyone, receives no evaluation traffic, or was created and never used. Stale flags accumulate as technical debt, because every one is a conditional branch engineers still have to read, test, and reason about. Featureflip detects them automatically by analyzing real evaluation traffic in each environment and assigning each flag a status of Active, Stale (signal persisted for 30 days), or Dead (signal persisted for 90 days), with the specific reasons attached. ### How does Featureflip detect stale and dead flags? Featureflip analyzes the evaluation traffic each flag actually receives in each environment. No agents, build steps, or configuration are required. Every hour, each flag-environment is checked for four signals: fully rolled out (enabled and serving one variation to at least 99.9% of evaluations), rolled back (disabled with at most one variation served), no traffic (enabled but receiving zero evaluations), and never used (created but never evaluated). A signal that persists across a 30-day window marks the flag Stale; across a 90-day window it marks the flag Dead. The flag list shows a badge with the worst status across environments, and the flag detail view breaks the status down per environment. ### What is the difference between a stale flag and a dead flag? The difference is how long the flag has shown no real decision-making activity. Stale means a staleness signal, such as serving a single variation to effectively all users or receiving no traffic, has persisted for 30 days. Dead means the same signal has persisted for 90 days. Stale is the early warning: the flag is probably finished but recent enough that the team still has context. Dead is the strong signal: the flag has not made a meaningful decision in three months and is a clear candidate for removal from code and then from the platform. ### Does Featureflip scan my codebase for flag references? No. Featureflip analyzes evaluation traffic, not source code, so it knows how flags behave in running systems but not where they are referenced in your repository. The two approaches are complementary: traffic analysis tells you which flags have stopped making decisions in production, and a code search tells you where each flag lives so you can remove it. Before deleting a flag, find and remove its references with a code search, then delete the flag from Featureflip. Removing the code first prevents the SDK from silently serving fallback values. --- ## Source: https://featureflip.io/docs/concepts/targeting/ # User Targeting & Segments Targeting lets you control which users see which variation of a feature flag. Instead of turning a flag on for everyone, you can serve different variations based on who the user is, what plan they are on, where they are located, or any other attribute you define. ## Evaluation context When your application evaluates a flag, it passes an **evaluation context** — a set of key-value attributes describing the current user or request. The SDK sends this context to the Featureflip evaluation service, which uses it to determine the correct variation. ```js const context = { user_id: "user-456", email: "jane@example.com", country: "US", plan: "pro", }; const showFeature = client.boolVariation("new-dashboard", context, false); ``` You decide what attributes to include. Common choices are user ID, email, country, plan tier, organization ID, and device type. The more attributes you provide, the more flexible your targeting can be. The `user_id` attribute is especially important — it is used for consistent percentage rollouts (see [Rollout Strategies](/docs/concepts/rollout-strategies/) ). ## Targeting rules A targeting rule matches users based on their context attributes and serves them a specific variation. Each rule consists of: - **Attribute** — the context field to evaluate (e.g., `plan`) - **Operator** — how to compare (e.g., `equals`, `contains`, `startsWith`, `endsWith`, `in`) - **Values** — what to compare against (e.g., `"pro"`, `"enterprise"`) - **Variation** — which variation to serve when the rule matches For example, a rule might say: “If `country` is in `["US", "CA"]`, serve variation `true`.” You can add multiple rules to a flag in each environment. Rules give you fine-grained control without changing application code. ## Rule evaluation order Rules are evaluated **top to bottom** in the order they appear. The first rule that matches wins, and its variation is served. Remaining rules are skipped. If no rules match and the flag is enabled, the **fallthrough variation** is served. This is the default experience for users who are not specifically targeted. The order matters. Place more specific rules above broader ones: - Rule: `email` equals `admin@company.com` — serve `true` (specific user) - Rule: `plan` equals `enterprise` — serve `true` (all enterprise users) - Fallthrough: serve `false` (everyone else) You can reorder rules in the Featureflip dashboard by dragging them. ## User segments A **segment** is a reusable group of users defined by attribute rules. Segments let you avoid duplicating the same targeting conditions across multiple flags. For example, you might create a segment called **Beta Users** with the rule: `plan` equals `"beta"` OR `email` ends with `"@yourcompany.com"`. Then, instead of recreating these conditions on every flag, you reference the “Beta Users” segment in your targeting rules. Segments are useful for: - **Beta programs** — define who is in the beta once, reference it everywhere - **Internal teams** — target your own employees for dogfooding - **Plan-tier entitlements** — group enterprise, pro, and free users for different experiences When you update a segment’s definition, the change takes effect across all flags that reference it. This makes maintenance much simpler than copying rules between flags. See [How to Use Segments](/docs/guides/segments/) for a step-by-step guide. ## Example flow Here is how targeting works end-to-end: - Your app calls `boolVariation("new-checkout", { user_id: "123", plan: "pro" }, false)`. - The SDK sends the evaluation request with the context to Featureflip. - Featureflip looks up the “new-checkout” flag in the current environment. - The flag is enabled. Featureflip evaluates targeting rules top to bottom. - **Rule 1:** `plan` equals `"pro"` — serve `true`. The context has `plan: "pro"`. Match! - Featureflip returns `true`. The user sees the new checkout. If the user had `plan: "free"` and no other rules matched, Featureflip would serve the fallthrough variation instead. The third argument to `boolVariation` (`false` in this example) is the **SDK default** — the value returned if the SDK cannot reach Featureflip or the flag does not exist. It is a safety net, not a targeting mechanism. ## Next steps - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — step-by-step guide to creating rules in the dashboard - [How to Use Segments](/docs/guides/segments/) — create reusable groups for targeting - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/evaluate/) — REST API reference for flag evaluation ## Frequently Asked Questions ### What is user targeting in feature flags? User targeting lets you control which users see a specific feature variation based on attributes you provide at evaluation time — like user ID, email address, country, subscription plan, or any custom property your application tracks. Instead of rolling a feature out to everyone at once, you can restrict it to a specific customer segment, a geographic region, or users on a particular plan tier. In Featureflip, you define targeting rules per flag per environment. Each rule specifies an attribute, a comparison operator (such as equals, contains, startsWith, or in), and a set of values to match against. When a user’s evaluation context satisfies the rule, they receive the configured variation. Rules are evaluated top to bottom, and the first match wins. This approach lets you do safe internal dogfooding, controlled beta programs, regional feature launches, and enterprise-tier gating — all without changing or redeploying application code. ### How do percentage rollouts work? Percentage rollouts use consistent hashing on a user identifier — typically user_id — to deterministically assign each user to a numbered bucket between 0 and 100. Featureflip combines the user ID with the flag key and hashes them together, so the assignment is unique per flag. If you configure a 30% rollout, users whose hash falls in the 0-30 range receive the enabled variation; the rest receive the control. Because the hash is deterministic, the same user always lands in the same bucket for the same flag — they will not flip between variations across page loads, sessions, or API calls. When you expand from 30% to 50%, you are widening the bucket window; users already in the 30% range stay in. You must include a user_id in your evaluation context for percentage rollouts to work correctly — without a stable identifier, Featureflip cannot produce a consistent hash. ### What are segments? Segments are reusable groups of users defined by attribute-based rules. Instead of duplicating the same targeting conditions across every flag that needs them, you define the group once as a segment and reference it by name in any flag’s targeting rules. For example, you might create a Beta Users segment with the rule: plan equals beta OR email ends with @yourcompany.com. Any flag that needs to target beta users simply references this segment rather than recreating those conditions. When you update the segment definition — say, to add a new email domain — the change propagates immediately to every flag that uses it. Segments are particularly useful for beta programs, internal dogfooding groups, customer tier distinctions like enterprise versus pro, and regional user groups. In Featureflip, you manage segments from the project-level Segments page, and they are scoped per project but available across all environments and flags within that project. --- ## Source: https://featureflip.io/docs/guides/account-security/ # Account Security Featureflip gives you two ways to strengthen how you sign in: **two-factor authentication (2FA)** adds a one-time code on top of your password, and **social sign-in** lets you use your Google or GitHub account instead of a separate password. Both are managed from your account, and you can use either, both, or neither. ## Two-factor authentication Two-factor authentication asks for a one-time code from an authenticator app in addition to your password, so a leaked password alone is not enough to sign in. Featureflip uses standard time-based one-time codes (TOTP), which work with any authenticator app — Google Authenticator, 1Password, Authy, and others. ### Enabling two-factor authentication - Go to your **Profile** page. - Find the **Two-Factor Authentication** card and click **Set up two-factor**. - **Scan the QR code** with your authenticator app. If you cannot scan it, enter the shown key into the app manually. - Enter the **6-digit code** from your app and click **Enable**. - Featureflip shows your **recovery codes**. Save them somewhere safe — see below. Once enabled, every future sign-in asks for a code from your authenticator app after your password. ### Recovery codes When you enable 2FA, Featureflip gives you a set of **recovery codes**. Each code works **once** and is your backup way to sign in if you lose access to your authenticator app (for example, if you replace your phone). Store them somewhere separate from your password manager’s 2FA entry — printed, or in a secure note. If you use some of your codes or want a fresh set, click **Regenerate recovery codes** on the Two-Factor Authentication card and confirm with your password. Regenerating **invalidates your previous codes** and replaces them with a new set. ### Signing in with 2FA After you enter your email and password, Featureflip prompts for your one-time code. Open your authenticator app, enter the current 6-digit code, and you are signed in. If you cannot reach your authenticator, use one of your recovery codes instead. ### Disabling two-factor authentication - Go to your **Profile** page. - On the **Two-Factor Authentication** card, click **Disable two-factor**. - Enter your password to confirm. Two-factor authentication is turned off, and future sign-ins ask for your password only. ## Sign in with Google or GitHub You can sign up and sign in to Featureflip with your Google or GitHub account instead of a password. On the sign-in screen, click **Continue with Google** or **Continue with GitHub**, complete that provider’s consent screen, and Featureflip creates or signs you into your account from your verified email address there. - **New to Featureflip?** Continuing with a provider creates your account from your profile there, with no separate password to set. - **Already have a Featureflip account** under the same email address? If that account’s email is already verified, signing in with the provider links to it, so you can use either your password or the provider going forward. If the email on the existing account has not been verified, Featureflip will not automatically link the two, as a safeguard against account takeover. Verify the account first. - **Two-factor authentication still applies.** With 2FA enabled, Featureflip asks for your one-time code once the provider hands you back, exactly as it does after a password. ### What GitHub sign-in reads GitHub sign-in asks for the `user:email` scope, and Featureflip uses it to read your primary address along with GitHub’s own record of whether that address is verified. Google signs you in over OpenID Connect, which carries verification status in the sign-in response itself. GitHub uses OAuth 2.0, where that status is absent from the response, so the address has to be read from your GitHub account directly. That has two consequences. Your public GitHub profile address is ignored when it differs from your primary one, so your Featureflip account is always created under the address that was checked. And a GitHub account whose primary address is unverified is turned away, because nobody has confirmed that the person signing in controls it. Social sign-in is built on each provider’s OAuth. It is separate from single sign-on through your organization’s identity provider. [OIDC single sign-on](/docs/guides/single-sign-on/) is on the Business plan, with setup guides for [Okta](/docs/guides/sso-okta/) and [Microsoft Entra ID](/docs/guides/sso-entra-id/) , and SAML / SCIM on Enterprise. [Contact us](/contact/) if your organization needs SAML. ## Next steps - [Team Management](/docs/guides/team-management/) — invite members and manage roles for your organization - [Audit Log](/docs/guides/audit-log/) — review changes made across your flags and environments - [Security at Featureflip](/product/security/) — where your data is stored, how SDK keys limit exposure, and how to report a vulnerability --- ## Source: https://featureflip.io/docs/guides/analytics-integration/ # Analytics & Experimentation Integration Featureflip server SDKs can hand every flag evaluation to your own code as it happens, in-process, with no network egress owned by Featureflip. You decide where the data goes: an analytics pipeline, an experimentation tool, or a log line for debugging. You register one or more **inspectors** when you create the client. Each inspector is a plain function called synchronously on every `boolVariation` / `stringVariation` / `numberVariation` / `jsonVariation` / `variationDetail` call. Available in every server SDK — JavaScript (`@featureflip/js`), Node (`@featureflip/node`), Python, C#, Go, Java, PHP, and Ruby — and in every client SDK: Browser (`@featureflip/browser`), React (`@featureflip/react`), Swift, Android, and Flutter. Client SDKs receive pre-evaluated results from the Evaluation API rather than running a local evaluator, so their inspectors fire from a different point in the code — see Client SDKs below. ## The evaluation event Each inspector receives one object. The field names are the same in every SDK, adapted to each language’s naming convention (`flagKey` in JavaScript, `flag_key` in Python and Ruby, `FlagKey` in C# and Go): ```ts interface EvaluationEvent { flagKey: string; // the flag that was evaluated context: EvaluationContext; // the full context you passed (a read-only copy) value: unknown; // the value your code received (default applied) variationKey?: string; // the served variation ("on", "control", ...) reason: string; // why this value was served (see below) ruleId?: string; // set only when a targeting rule matched prerequisiteKey?: string; // set only when a prerequisite was not met timestamp: string; // ISO-8601 } ``` ### Reason values differ by SDK `reason` uses each SDK’s own native representation, because every SDK’s evaluator is the source of truth for its own runtime. The set of outcomes is identical everywhere; only the spelling changes: | SDK | reason is | Example | | --- | --- | --- | | JavaScript, Node, Ruby | a string | "RuleMatch", "PrerequisiteFailed" | | Python, Java | the SDK’s EvaluationReason enum | EvaluationReason.RULE_MATCH | | C# | the SDK’s EvaluationReason enum, PascalCase | EvaluationReason.RuleMatch | | Go | the SDK’s EvaluationReason constant | featureflip.ReasonRuleMatch | | PHP | a string, SCREAMING_SNAKE_CASE | 'RULE_MATCH', 'PREREQUISITE_FAILED' | | Browser, React, Swift, Android, Flutter (client SDKs) | a string, the engine’s own kebab-case, forwarded verbatim | "fallthrough", "rule-match:rule-abc-123", "prerequisite-failed" | Compare against your SDK’s own reason type rather than against a hardcoded string from another language’s example. `flag-not-found` is the one reason a client SDK **synthesizes** rather than forwards from the server — it’s the client-side spelling of the same `FlagNotFound` / `FLAG_NOT_FOUND` outcome the server SDKs’ local evaluators already report (see the reason table above). A client SDK synthesizes it locally (never sent by the server) whenever the flag key is absent from the client’s snapshot — unknown key, client not yet initialized, or the flag isn’t marked `clientSideVisible`. See Client SDKs for the full behavior. ## Register an inspector ### JavaScript / Node ```ts import { FeatureflipClient } from '@featureflip/node'; const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, inspectors: [ (event) => { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ], }); ``` { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ],});"> ### Python ```python from featureflip import Config, FeatureflipClient def log_exposure(event): print(f"[flag] {event.flag_key} -> {event.variation_key} ({event.reason})") client = FeatureflipClient( sdk_key=os.environ["FEATUREFLIP_SDK_KEY"], config=Config(inspectors=[log_exposure]), ) ``` {event.variation_key} ({event.reason})")client = FeatureflipClient( sdk_key=os.environ["FEATUREFLIP_SDK_KEY"], config=Config(inspectors=[log_exposure]),)"> ### C# ```csharp var options = new FeatureFlagOptions { Inspectors = { e => Console.WriteLine($"[flag] {e.FlagKey} -> {e.VariationKey} ({e.Reason})"), }, }; var client = FeatureflipClient.Get( Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY"), options); ``` Console.WriteLine($"[flag] {e.FlagKey} -> {e.VariationKey} ({e.Reason})"), },};var client = FeatureflipClient.Get( Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY"), options);"> ### Go ```go client, err := featureflip.Get( os.Getenv("FEATUREFLIP_SDK_KEY"), featureflip.WithInspectors(func(e featureflip.EvaluationEvent) { log.Printf("[flag] %s -> %s (%s)", e.FlagKey, e.VariationKey, e.Reason) }), ) ``` %s (%s)", e.FlagKey, e.VariationKey, e.Reason) }),)"> ### Java ```java FeatureflipClient client = FeatureflipClient.builder(System.getenv("FEATUREFLIP_SDK_KEY")) .inspectors(List.of(event -> System.out.printf("[flag] %s -> %s (%s)%n", event.getFlagKey(), event.getVariationKey(), event.getReason()))) .build(); ```  System.out.printf("[flag] %s -> %s (%s)%n", event.getFlagKey(), event.getVariationKey(), event.getReason()))) .build();"> ### PHP The PHP SDK is transport-agnostic: `cache`, `httpClient` and `requestFactory` are required, and you supply your own PSR-16 / PSR-18 / PSR-17 implementations. ```php $httpFactory = new \GuzzleHttp\Psr7\HttpFactory(); $config = new Config( cache: $yourPsr16Cache, httpClient: new \GuzzleHttp\Client(['timeout' => 30]), requestFactory: $httpFactory, streamFactory: $httpFactory, inspectors: [ function (EvaluationEvent $event): void { error_log("[flag] {$event->flagKey} -> {$event->variationKey} ({$event->reason})"); }, ], ); $client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), $config); ``` 30]), requestFactory: $httpFactory, streamFactory: $httpFactory, inspectors: [ function (EvaluationEvent $event): void { error_log("[flag] {$event->flagKey} -> {$event->variationKey} ({$event->reason})"); }, ],);$client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), $config);"> ### Ruby ```ruby config = Featureflip::Config.new( inspectors: [ ->(event) { puts "[flag] #{event.flag_key} -> #{event.variation_key} (#{event.reason})" } ] ) client = Featureflip::Client.get(ENV["FEATUREFLIP_SDK_KEY"], config: config) ``` (event) { puts "[flag] #{event.flag_key} -> #{event.variation_key} (#{event.reason})" } ])client = Featureflip::Client.get(ENV["FEATUREFLIP_SDK_KEY"], config: config)"> Inspectors are honored on the first client created for a given SDK key. Pass a list; every callable in it is called on each evaluation, in registration order. Entries that aren’t callable are ignored rather than causing an error at evaluation time. ## Client SDKs Browser, React, Swift, Android, and Flutter also support `inspectors`, with the identical `EvaluationEvent` shape described above. Register one the same way you would on a server SDK: ### Browser ```ts import { FeatureflipClient } from '@featureflip/browser'; const client = FeatureflipClient.get({ clientKey: 'your-client-sdk-key', inspectors: [ (event) => { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ], }); ``` { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ],});"> ### React ```tsx import { FeatureflipProvider } from '@featureflip/react'; function App() { return ( { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ]} > {/* rest of your app, including components that call useFeatureFlag */} ); } ``` { console.log(`[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})`); }, ]} > {/* rest of your app, including components that call useFeatureFlag */}  );}"> ### Swift ```swift import Featureflip let config = FeatureflipConfig( clientKey: "your-client-sdk-key", inspectors: [ { event in print("[flag] \(event.flagKey) -> \(event.variationKey ?? "nil") (\(event.reason))") } ] ) let client = FeatureflipClient(config: config) ``` \(event.variationKey ?? "nil") (\(event.reason))") } ])let client = FeatureflipClient(config: config)"> ### Android ```kotlin import dev.featureflip.android.FeatureflipClient import dev.featureflip.android.FeatureflipConfig val config = FeatureflipConfig( clientKey = "your-client-sdk-key", inspectors = listOf( { event -> println("[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})") }, ), ) val client = FeatureflipClient.get(config) ``` println("[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})") }, ),)val client = FeatureflipClient.get(config)"> ### Flutter ```dart import 'package:featureflip/featureflip.dart'; final config = FeatureflipConfig( clientKey: 'your-client-sdk-key', inspectors: [ (event) { print('[flag] ${event.flagKey} -> ${event.variationKey} (${event.reason})'); }, ], ); final client = FeatureflipClient.get('your-client-sdk-key', config: config); ``` ${event.variationKey} (${event.reason})'); }, ],);final client = FeatureflipClient.get('your-client-sdk-key', config: config);"> A few things differ from the server SDKs described above. ### A different insertion point Server SDKs evaluate locally, so their inspectors fire from inside the evaluator itself. Client SDKs never evaluate — they hold a snapshot of pre-evaluated results pushed down from the Evaluation API, and the app just reads out of it. So a client-SDK inspector fires from the 4 variation accessors (`boolVariation` / `stringVariation` / `numberVariation` / `jsonVariation`), *after* the accessor has type-checked the stored value and decided what to hand back to your code. Whichever snapshot-reading accessor a given SDK exposes — `flagDetail(key)` on Browser/Swift/Android, `allFlags()` on Flutter, or the underlying Browser client’s `flagDetail(key)` reached via React’s `useFeatureflipClient()` — is deliberately silent: it’s a diagnostic read of the snapshot, not an evaluation your app acted on. ### Reason representation, plus a synthesized flag-not-found Because the client SDK’s “evaluator” is just the server’s response, `reason` is the engine’s kebab-case string forwarded exactly as received: `fallthrough`, `flag-disabled`, `rule-match:{id}`, `prerequisite-failed`, `error` (see the reason table above). `flag-not-found` is the one reason a client SDK **synthesizes** rather than forwards from the server — it’s the client-side spelling of the same `FlagNotFound` / `FLAG_NOT_FOUND` outcome the server SDKs’ local evaluators already report — whenever the requested key isn’t in the client’s snapshot: an unknown key, a client that hasn’t finished initializing yet, or a flag that isn’t `clientSideVisible` (client SDKs only ever receive client-visible flags). On `flag-not-found`, `variationKey`, `ruleId`, and `prerequisiteKey` are all unset. `ruleId` is derived the same way in every client SDK: it’s parsed out of a `rule-match:{id}` reason and left unset for every other reason — `reason` itself is never rewritten. Because the parsing rule and the event’s field names are identical to the server SDKs, one inspector function works unchanged whether it’s registered against a server SDK or a client SDK — including the PostHog/Amplitude/GrowthBook examples below and the `prerequisiteKey` debugging example at the end of this page. ### Type mismatch: the one field combination that surprises people Reading a flag with the wrong accessor — say, `boolVariation` on a flag whose value is actually a string — makes the accessor fall back to *your* default. The inspector event reports that fallback as `value`, but `reason` and `variationKey` still describe what the server actually served, because the server-side evaluation genuinely happened; only the local accessor rejected the type. Concretely, for a snapshot flag `{ value: "hello", variation: "v1", reason: "rule-match:rule-abc-123" }`: ```ts client.boolVariation('string-flag', false); // returns false — your default, because the stored value isn't a boolean // but the inspector event still shows what the server served: // { // flagKey: 'string-flag', // value: false, // <- your default, NOT the server's value // variationKey: 'v1', // <- a real variation WAS served // reason: 'rule-match:rule-abc-123', // <- a real rule DID match // } ``` That combination — `value` equal to your default while `variationKey`/`reason` describe a live, non-off variation — *is* the type-mismatch signal. Don’t confuse it with `flag-not-found`: a missing flag also serves your default, but leaves `variationKey` unset and reports the synthesized `flag-not-found` reason instead of a real one. ### The singleton foot-gun: inspectors is first-caller-wins Every client SDK is singleton-by-construction: the underlying core is refcounted and shared per client key, so the first call to `FeatureflipClient.get(...)` (Swift: the first `FeatureflipClient(config:)`; React: the first `` mounted for a `clientKey`) constructs the shared core, and **that call’s inspectors are the only ones that will ever fire for that key.** A second `get()` (or provider mount) with the same client key — even one that passes different or additional `inspectors` — silently returns a handle to the already-constructed core and inherits whatever the first caller registered. On Browser, React, Android, and Flutter there is no warning specific to `inspectors`: config fields that *are* structurally compared (`baseUrl`, `streaming`, …) log a mismatch warning there, but `inspectors` is deliberately excluded from that comparison because functions aren’t structurally comparable. Swift is a step further removed: its core-acquisition path does no config comparison at all, for any field, so a repeat `FeatureflipClient(config:)` call for a live `clientKey` silently reuses the first config in every respect — not just `inspectors` — with no warning of any kind. In practice: register every inspector your app needs at the one place that constructs the client for a given key. If your analytics wiring and your debug logging live in separate files, make sure only one of them actually calls `get()`/mounts the provider, and pass both inspectors there — a second, independent construction call for the same key will not add to the list. ## Pipe exposures into your tools ### PostHog ```ts inspectors: [ (event) => { posthog.capture({ distinctId: String(event.context.user_id ?? 'anonymous'), event: '$feature_flag_called', properties: { $feature_flag: event.flagKey, $feature_flag_response: event.value, variation: event.variationKey, reason: event.reason, }, }); }, ] ``` { posthog.capture({ distinctId: String(event.context.user_id ?? 'anonymous'), event: '$feature_flag_called', properties: { $feature_flag: event.flagKey, $feature_flag_response: event.value, variation: event.variationKey, reason: event.reason, }, }); },]"> ### Amplitude ```ts inspectors: [ (event) => { amplitude.track('flag_evaluated', { flag_key: event.flagKey, variation: event.variationKey, value: event.value, reason: event.reason, }, { user_id: String(event.context.user_id ?? '') }); }, ] ``` { amplitude.track('flag_evaluated', { flag_key: event.flagKey, variation: event.variationKey, value: event.value, reason: event.reason, }, { user_id: String(event.context.user_id ?? '') }); },]"> ### GrowthBook (or any experiment tool) ```ts inspectors: [ (event) => { experimentTracker.recordExposure({ unit: String(event.context.user_id ?? ''), experiment: event.flagKey, variation: event.variationKey ?? 'default', }); }, ] ``` { experimentTracker.recordExposure({ unit: String(event.context.user_id ?? ''), experiment: event.flagKey, variation: event.variationKey ?? 'default', }); },]"> ## Rules - **Errors are isolated.** If an inspector fails — a thrown exception, a Go panic, a Ruby raise — evaluation still returns the correct value, the other inspectors still run, and the SDK logs a warning. Your analytics failures never break a feature flag. The isolation guarantee holds identically on every client SDK, including Swift; the warning-on-throw specifically is Browser/React/Android/Flutter only — Swift’s `EvaluationInspector` is a non-throwing closure type, so an inspector cannot throw in the first place and there is nothing to catch or warn about. - **Keep inspectors fast.** They run synchronously in the evaluation call. For anything slow, enqueue the event and process it off the hot path. - **Treat context as read-only.** It is a shallow copy of the context you passed; mutating it has no effect on the SDK. ## Debugging with inspectors Because `reason`, `ruleId`, and `prerequisiteKey` are on the event, an inspector answers “why did this user get variation X?” without any extra tooling. The example below is the JS/Node server SDK; use your SDK’s own reason type for the comparison (see Reason values differ by SDK ) — note that even though Browser and React are also JavaScript, their `reason` is the engine’s kebab-case string (`'prerequisite-failed'`, not `'PrerequisiteFailed'`), because a client SDK forwards the server’s reason verbatim instead of running its own evaluator: ```ts inspectors: [ (event) => { if (event.reason === 'PrerequisiteFailed') { console.warn(`${event.flagKey} served its off value: prerequisite "${event.prerequisiteKey}" was not met`); } }, ] ``` { if (event.reason === 'PrerequisiteFailed') { console.warn(`${event.flagKey} served its off value: prerequisite "${event.prerequisiteKey}" was not met`); } },]"> --- ## Source: https://featureflip.io/docs/guides/audit-log/ # Feature Flag Audit Log Featureflip records every change to your flags and configuration automatically — who made it, what changed, and when. There is nothing to enable: the audit log captures changes across your organization from the moment you start using Featureflip, so when something breaks you can answer “what changed, and who changed it?” without guesswork. ## What gets recorded Every create, update, and delete is captured across the entities that drive flag behavior: - **Feature flags** — created, deleted, toggled on or off, renamed, retargeted - **Variations** — added, edited, or removed - **Targeting rules** — created, reordered, or changed - **Per-environment flag configuration** — the on/off state and rollout for each environment - **Environments** — created, renamed, or deleted - **User segments** — created, edited, or deleted - **SDK keys** — created, rotated, or revoked - **Projects** and the **organization** itself For an **update**, each changed property is recorded on its own, with the **previous value and the new value**, so you can see exactly what moved — for example, a rollout going from 25% to 100%, or a flag flipping from enabled to disabled. Every entry also stores: - **Who** — the actor’s name and email (preserved on the entry even if the member later leaves) - **When** — a precise timestamp - **Request metadata** — IP address and user agent of the request that made the change ## Where to find it There are two views, both in the [Featureflip dashboard](https://app.featureflip.io/) . ### Organization-wide audit log The **Audit Log** page shows every recorded change across your organization, newest first. Filter the list to narrow an investigation: - by **project** - by **entity** (a specific flag, segment, environment, and so on) or **entity type** - by **action** (created, updated, deleted) - by **user** - by **date range** Results are paginated, so a high-traffic project stays navigable. ### Per-flag history Every flag has a **History** tab on its detail page. It shows a complete timeline for that one flag — including changes to everything beneath it: its variations, its per-environment configuration, and its targeting rules. During an incident this is usually the fastest path to “who changed this flag, and when,” without scrolling the organization-wide log. The flag history can be filtered by action and date range as well. ## Retention by plan Audit log history is retained based on your plan, and entries older than your retention window are removed automatically: | Plan | Audit log retention | | --- | --- | | Free | 7 days | | Team | 30 days | | Pro | 30 days | | Business | 90 days | | Enterprise | 365 days (custom policies available) | If you need to keep records beyond your plan window — for a compliance review or a post-incident archive — move to a plan with a longer retention period. See [pricing](/pricing/) for the current plans. ## Using the audit log for incident review When a release goes wrong, the audit log turns “something changed” into a specific, attributable fact: - Open the affected flag and switch to its **History** tab. - Find the change that lines up with when behavior shifted — a toggle, a rollout bump, a rule edit. - Read the **before and after** values to confirm what actually changed, and who changed it. Because a disabled flag deterministically serves its off variation, pairing the audit log with the [kill switch](/docs/concepts/feature-flags/#why-use-feature-flags) pattern gives you both halves of incident response: turn the feature off in seconds, then have the record of who turned it off and when for the post-incident review. ## Next steps - [Audit Log (feature overview)](/product/features/audit-log/) — how audit logging fits with the rest of the platform - [Team Management](/docs/guides/team-management/) — invite members and manage roles - [Reliability & Resilience](/docs/concepts/reliability/) — how flags fail safe when the service is unavailable - [Stale Flag Detection](/docs/concepts/stale-flags/) — find flags that no longer make a real decision - [Creating Flags](/docs/guides/creating-flags/) — create and configure flags in the dashboard ## Frequently Asked Questions ### Does Featureflip log who changed a feature flag? Yes. Featureflip records every change automatically, with no configuration required. Each audit entry captures who made the change (name and email), what changed (the entity and, for edits, the specific property with its before and after values), and when it happened. Toggling a flag, editing a targeting rule, changing an environment configuration, creating or deleting a segment, and rotating an SDK key are all recorded. You can review the log organization-wide or scoped to a single flag from the Featureflip dashboard. ### How long does Featureflip keep audit logs? Audit log retention depends on your plan: 7 days on Free, 30 days on Team and Pro, 90 days on Business, and 365 days on Enterprise. Entries older than your retention window are removed automatically. If you need to keep records longer, move to a plan with a longer window. ### Can I see the full history of a single feature flag? Yes. Every flag has a History tab that shows a complete timeline for that flag, including changes to the flag itself and to everything beneath it — its variations, its per-environment configuration, and its targeting rules. This is the fastest way to answer who changed this flag, and when, during an incident review, without scrolling the organization-wide log. ### What changes are recorded in the Featureflip audit log? Featureflip records create, update, and delete actions across feature flags, variations, targeting rules, per-environment flag configurations, environments, user segments, SDK keys, projects, and the organization itself. For updates, each changed property is recorded individually with its previous and new value. Each entry also stores the actor, a timestamp, and request metadata such as IP address and user agent. --- ## Source: https://featureflip.io/docs/guides/cleanup-notices/ # Flag Cleanup Notices: Emails for Flags to Remove A flag nobody is watching tends to stay in the code for good. Cleanup notices are an email that tells someone when a flag is ready to come out, so they find out without having to go and look. They’re part of Team and every plan above it. ## Which plans send them Team, Pro, Business and Enterprise get the emails. Free doesn’t. The flags a notice would have named are still there to find on Free: the flags list marks each expired or dead one and can filter down to just those, `find_stale_flags` returns the same set over the [MCP server](/docs/integrations/mcp/) and the Management API, and the [flag cleanup Action](/docs/integrations/flag-cleanup-action/) opens a removal pull request for a dead flag on any plan. Upgrading from Free won’t land you a backlog email. Anything that was already expired or dead at the last daily check before the upgrade stays quiet, and notices begin with the first flag that qualifies afterwards. ## What triggers a notice A flag gets a notice when either of these becomes true: - **It has passed its expiry date.** Featureflip reports it stale with the reason `PastExpiry` as soon as the day ends, and the next notice run picks it up. - **It has gone Dead in every environment.** [Stale flag detection](/docs/concepts/stale-flags/) marks an environment Dead after 90 days of one of these: the flag was never evaluated there after being created, it stayed enabled there with no evaluations at all, it served one variation to effectively everyone, or it sat disabled serving its off variation. If even one environment is still in use, there’s no notice. A flag that production depends on isn’t ready to remove just because nobody touches it in development. A flag that is only Stale for a traffic reason, which takes 30 days, doesn’t get a notice yet. It shows up in the dashboard and in `find_stale_flags`, and the email waits until the signal has held for the full 90 days. An expiry date is the exception, because a date someone set on purpose is already a clear answer. Archived flags never get notices. ## When notices arrive Featureflip checks once a day at 14:00 UTC and sends each recipient a single digest for each organization, listing every flag that has newly qualified. The subject reads “Feature flag checkout-redesign is ready to remove” for one flag, or “3 feature flags are ready to remove” for several. Inside, flags are grouped by project. Each entry links to the flag in the dashboard and says why it’s listed, and in which environments, for example “Expired on 2026-12-01”, “No evaluations recently (Production)” or “Never evaluated since it was created (Development, Staging)”. You hear about each flag once, rather than every day. It stays quiet while the flag keeps qualifying. If you fix the reason (traffic comes back, say) and later the flag qualifies again, a fresh notice goes out, though never sooner than 30 days after the last one for that flag. Expiry dates skip that wait. If you push a flag’s expiry date out after its notice and the new date passes too, the next notice run sends a fresh one. A date only passes twice when somebody moves it, so it can’t flicker the way a traffic signal can. ## Who receives them Who gets the email depends on the plan and on whether the flag has an [owner](/docs/guides/flag-owners/) . | | Flag has an owner | Flag has no owner | | --- | --- | --- | | Pro, Business, Enterprise | The owner | Every Owner and Admin in the organization | | Team | Every Owner and Admin | Every Owner and Admin | On Pro and above, a digest can have up to three sections: **Flags you own**; **Flags with no owner**, which explains that they’re coming to you as an admin and that assigning an owner routes future notices to that person; and **Flags whose owner isn’t getting these notices**, for flags whose owner has turned notices off. On Team, every flag is listed under **Flags in your organization**. An owner who has left the organization, has been deactivated, or has scheduled their account for deletion can’t receive a notice, so their flags go to the Owners and Admins instead. ## Turning notices off Each person chooses for themselves: - **In the app.** Open your profile, find **Email Preferences**, and switch off **Flag cleanup notices**. It’s on by default. - **From the email.** Every notice ends with **Unsubscribe from flag cleanup notices**. The link opens a page asking “Stop flag cleanup notices?” and unsubscribes you when you confirm. Mail clients that offer a one-click unsubscribe button use the same link. Either way, only cleanup notices stop. Account and security emails still arrive, and you can switch the notices back on from **Email Preferences** at any time. Opting out only stops the email coming to you. If a flag’s owner turns notices off, that flag goes to the organization’s Owners and Admins instead, as if it had no owner. When an admin opts out, the other Owners and Admins keep getting those flags. ## Next steps - [Flag Owners](/docs/guides/flag-owners/) : name who each notice should go to - [Flag Expiration Dates](/docs/guides/flag-expiration/) : set the date that triggers a notice - [Stale Flag Detection](/docs/concepts/stale-flags/) : every staleness reason, and how Dead differs from Stale - [Flag Cleanup Action](/docs/integrations/flag-cleanup-action/) : open the pull request that removes the flag from your code --- ## Source: https://featureflip.io/docs/guides/creating-flags/ # How to Create Feature Flags Feature flags are the core building block of Featureflip. This guide walks you through creating, configuring, and managing flags from the web dashboard. ## Creating a flag - Navigate to your project’s **Flags** page at `/flags`. - Click **Create Flag** in the top right. - Fill in the flag details: - **Name** — A human-readable label (e.g., “New Checkout Flow”). - **Key** — Auto-generated from the name (e.g., `new-checkout-flow`). This is what you reference in code. You can edit it before saving. - **Flag type** — Choose from Boolean, String, Number, or JSON. - **Description** (optional) — Explain what this flag controls and why it exists. - **Tags** (optional) — Add tags to organize and filter flags (e.g., `frontend`, `experiment`, `checkout`). - Click **Create** to save the flag. ## Default variations When you create a flag, default variations are set up based on the type: - **Boolean** flags get `true` and `false` variations automatically. - **String**, **Number**, and **JSON** flags start with placeholder variations that you customize on the flag detail page. ## Adding variations For non-boolean flags, you can add more variations from the flag detail page: - Open the flag by clicking its name on the Flags page. - Scroll to the **Variations** section. - Click **Add Variation**. - Enter a **key** (used internally) and a **value** (what gets returned to your application). - Save your changes. ## Toggling a flag Each flag can be toggled on or off independently per environment: - Open the flag detail page at `/flags/:flagId`. - Select the environment tab (e.g., Development, Staging, Production). - Use the toggle to turn the flag on or off. When a flag is **off**, the off variation is served to all users regardless of targeting rules. When a flag is **on**, targeting rules and the fallthrough variation determine what each user receives. ## Archiving and restoring Archive flags you no longer need instead of deleting them immediately: - Open the flag detail page or select flags from the list. - Click **Archive**. The flag stops being evaluated but is not permanently removed. - To bring it back, find the flag in the archived list and click **Restore**. - Delete a flag permanently only when you are certain it will never be needed again. Because archived flags are no longer evaluated, archive a flag only after you have removed its references from your application code. A flag cannot be archived while other active flags use it as a prerequisite, and cannot be deleted while any flag still references it as a prerequisite — remove those prerequisites first. Archiving a dependency chain in one bulk operation works, as long as every dependent flag is included in the selection. Restoring follows the same rule in reverse: a flag whose prerequisites are still archived cannot be restored until those prerequisites are restored. ## Bulk operations Select multiple flags from the Flags list to perform actions in bulk: - **Archive** — Remove several flags from active evaluation at once. - **Restore** — Bring back multiple archived flags. - **Delete** — Permanently remove selected flags. - **Update tags** — Add or remove tags across multiple flags. Check the boxes next to the flags you want, then use the bulk action toolbar that appears. ## Best practices - **Use descriptive names.** Anyone on your team should understand what a flag controls from its name alone. - **Prefix keys with the feature area.** For example, `checkout-new-flow` or `billing-annual-discount`. This makes keys easier to find in code. - **Add descriptions.** Document why a flag exists and when it should be removed. - **Tag consistently.** Agree on a tagging convention with your team (e.g., by team, by feature area, or by lifecycle stage). - **Clean up old flags.** Archive flags once a feature is fully rolled out. Stale flags add confusion and technical debt. - **Review flags regularly.** Schedule periodic reviews to identify flags that are no longer needed. ## Next steps - [What Are Feature Flags?](/docs/concepts/feature-flags/) — understand flag types, variations, and lifecycle - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — control who sees which variation - [How to Set Prerequisite Flags](/docs/guides/prerequisites/) — gate one flag on another before its rules run - [How to Manage Environments](/docs/guides/environments/) — configure flags per environment - [SDK Overview](/docs/sdks/overview/) — integrate flags in your application code --- ## Source: https://featureflip.io/docs/guides/environments/ # How to Manage Environments Environments let you maintain separate feature flag configurations for each stage of your development workflow. A flag can be enabled in Development, disabled in Staging, and gradually rolled out in Production — all independently. ## Default environments Every new project starts with three environments: - **Development** — For local development and testing. - **Staging** — For pre-production validation. - **Production** — For live user traffic. These defaults cover the most common workflow. You can create additional environments as needed. ## Creating an environment - Navigate to the **Environments** page at `/environments`. - Click **Create Environment**. - Enter a **name** (e.g., “QA” or “Canary”) and a **key** (used to identify the environment in API calls). - Click **Create**. The new environment immediately gets configurations for all existing flags. All flags start **disabled** in the new environment, so there is no risk of accidental exposure. ## SDK keys Each environment has its own SDK keys that your application uses to connect to the Featureflip Evaluation API. - Go to the **SDK Keys** page at `/sdk-keys`. - Select the environment you need a key for. - Click **Create Key** to generate a new SDK key. - Copy the key and add it to your application’s configuration. You can create multiple keys per environment (e.g., for different services or deployments). Revoke a key if it is compromised or no longer needed — revoked keys stop working immediately. ## Environment-specific configuration Each flag has independent settings per environment: - **On/off state** — A flag can be on in Development and off in Production. - **Targeting rules** — Different rules for different environments. Test aggressive targeting in Staging before applying it to Production. - **Fallthrough variation** — The default variation served when no rules match can differ per environment. Changes in one environment **never affect** other environments. This isolation lets you test freely in Development without risking Production. ## Deleting environments Deleting an environment permanently removes: - All flag configurations for that environment. - All SDK keys associated with the environment. This action **cannot be undone**. Make sure no applications are still using SDK keys from the environment before deleting it. - Navigate to the Environments page. - Click the environment you want to remove. - Click **Delete** and confirm the action. ## Next steps - [Environments](/docs/concepts/environments/) — understand how environments work and best practices - [How to Create Feature Flags](/docs/guides/creating-flags/) — create flags that are configured per environment - [SDK Overview](/docs/sdks/overview/) — learn how SDK keys connect your app to an environment - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — configure environment-specific targeting --- ## Source: https://featureflip.io/docs/guides/flag-expiration/ # Flag Expiration Dates Most feature flags are meant to be temporary. An expiration date makes that plan explicit: set the day you expect a flag to come out, and Featureflip reports it as stale once that day passes, so it turns up in cleanup work instead of sitting forgotten for months. An expiry date works as a reminder. Passing the date never changes what the flag serves. See What happens when the date passes below. Expiration dates are available on every plan, Free included. ## Setting an expiry date ### When you create a flag The **Create Flag** page includes an optional **Expires on** field. Pick a date and the field explains what it does: past this date, the flag is reported as stale, and evaluation does not change. Leave it blank for a flag with no planned end date. ### On an existing flag Open the flag and look at its header. A flag with no expiry shows **Set expiry**: click it, pick a date, and save. A flag that already has one shows **Expires** followed by the date, with an **Edit expiry** control next to it. You can’t set or change an expiry on an archived flag. ## Changing or removing an expiry date From the same header control: - **Change the date.** Click **Edit expiry**, pick a new date, and **Save**. - **Remove it entirely.** Click **Edit expiry**, then **Remove**. Removing is always allowed, even after the date has passed, so you can back out of a commitment you no longer want to make. ## Setting an expiry through the API Expiry has its own endpoints, separate from the main flag update call. That way, other tools that write to your flags, including the Terraform provider, never clear a date by accident just because they don’t know the field exists. Set the date at creation: ```plaintext POST /api/v1/orgs/{org}/projects/{project}/flags ``` ```json { "key": "checkout-redesign", "name": "Checkout Redesign", "type": "Boolean", "expiresAtUtc": "2026-12-01" } ``` Or set it on an existing flag: ```plaintext PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/expiry ``` ```json { "expiresAtUtc": "2026-12-01" } ``` A date on its own means the end of that day in UTC, exactly what the dashboard sets when you pick it. You can send a full timestamp such as `2026-12-01T17:00:00Z` instead, and it’s stored as written. Clear it with: ```plaintext DELETE /api/v1/orgs/{org}/projects/{project}/flags/{flag}/expiry ``` See the [API Reference](/docs/management-api/reference/) for the full request and response shapes, and [Authentication](/docs/management-api/authentication/) for how to get a token. ## Setting an expiry from an AI agent The [MCP server](/docs/integrations/mcp/) has a `set_flag_expiry` tool that sets or clears the date, and `create_flag` takes an `expiresAtUtc` option, so you can ask Claude Code or Cursor to date a flag while it writes the code behind it. `find_stale_flags` lists every expired flag, however recently someone edited it. ## Validation - **The date must be in the future when you set it.** A past or missing date is refused. Today counts, because the flag doesn’t expire until the day ends. A date you set earlier that has since passed stays stored and valid. Nothing forces you to touch it the moment it expires. - **Dates apply as whole UTC days.** Whatever date you pick is treated as a UTC calendar day, and the flag expires at the very end of that day in UTC. This keeps the date consistent no matter which timezone you or a teammate happens to be in. A date sent through the API or the MCP server works the same way. The Terraform provider only takes a full timestamp, so write `2026-12-01T23:59:59Z` there to get the same result. - **Removing an expiry is never date-checked.** You can clear an already-expired date at any time. ## What happens when the date passes Nothing about evaluation changes. The flag keeps serving exactly what it served the day before: the same rules, the same rollout percentage, whatever targeting was already in place. An expiry date tells your team when a flag is meant to come out. Want Featureflip to actually flip a flag on or off at a set time? Use [scheduled changes](/docs/guides/scheduled-changes/) for that. What does change is how the flag is reported: - It’s added to [stale flag detection](/docs/concepts/stale-flags/) with the reason `PastExpiry`, always at the Stale tier, whether or not the flag has any real traffic. - The flag detail header switches from **Expires** to **Expired**, shown in amber. - The flags list shows a compact **Expired** marker. - The staleness badge’s tooltip lists `PastExpiry` alongside any other reason already flagging that flag. - On Team and above, a [cleanup notice](/docs/guides/cleanup-notices/) email goes out at the next daily run, to the flag’s [owner](/docs/guides/flag-owners/) on Pro and above and to your organization’s Owners and Admins on Team. Free sends no email, and the **Expired** marker is how you find out. Detection runs on the same hourly cycle as the rest of staleness analysis, so a newly passed date can take up to an hour to show up as stale. Extending the date into the future or removing it clears the stale marking right away, without waiting for the next cycle. Every expiry change is recorded in the flag’s [audit log](/docs/guides/audit-log/) like any other field edit. ## How this fits with cleanup `PastExpiry` is one signal among several that [stale flag detection](/docs/concepts/stale-flags/) tracks, and it behaves a little differently from the traffic-based ones. The other four signals need to hold steady for 30 or 90 days before they mark a flag Stale or Dead. `PastExpiry` applies as soon as the date passes, with no waiting period, because a stated date is unambiguous in a way that traffic patterns aren’t. A passed expiry date alone never opens a pull request. The [flag cleanup Action](/docs/integrations/flag-cleanup-action/) works from traffic-based removal candidates, and a flag whose only stale reason is `PastExpiry` doesn’t qualify until an independent traffic signal agrees. That’s by design: an expiry date records intent, not proof the flag has stopped mattering, so a mistyped date should never make a flag look safe to delete. ## Next steps - [Stale Flag Detection](/docs/concepts/stale-flags/) : the full detection system, and how `PastExpiry` fits into it - [How to Create Feature Flags](/docs/guides/creating-flags/) : create the flags you’ll set expiry dates on - [Cleanup Notices](/docs/guides/cleanup-notices/) : the email that goes out once the date passes - [Flag Owners](/docs/guides/flag-owners/) : name who that email goes to - [Scheduled Changes](/docs/guides/scheduled-changes/) : actually turn a flag on or off at a future time - [Flag Cleanup Action](/docs/integrations/flag-cleanup-action/) : remove dead flag code by pull request - [API Reference](/docs/management-api/reference/) : the full expiry endpoint contract --- ## Source: https://featureflip.io/docs/guides/flag-owners/ # Flag Owners: Who Is Responsible for Each Flag Every flag should have someone who remembers why it exists and who takes it out when the job is done. A flag owner is that person, written down. Featureflip shows the owner on the flag, lets you filter the flags list by owner, and sends [cleanup notices](/docs/guides/cleanup-notices/) about a flag to its owner rather than to every admin in the organization. Flag owners are on the **Pro** plan and above. ## What an owner is An owner is one active member of your organization, attached to one flag. A flag has at most one owner, or none, in which case it shows as **Unowned**. Owning a flag grants no extra permissions. Anyone with the Member role or above can still edit it, toggle it or archive it, exactly as before. The owner is a pointer to the person accountable for the flag, and that’s all. When someone creates a flag, they become its owner unless they pick somebody else. A flag created with a service token starts unowned, since a token is not a person. ## Setting or changing an owner in the dashboard **When you create a flag.** The **Create Flag** page has an **Owner** field. It defaults to **Me (default)**, and the list holds every member of the organization. The hint under it reads “Who is accountable for this flag and for removing it when it is done.” **On an existing flag.** The flag header shows **Owner:** followed by a name, or **Unowned**. Click **Set owner** or **Change owner**, choose a member from **Choose a member**, and click **Save**. To take the owner off without naming a new one, open the same control and click **Clear owner**. You can’t change the owner of an archived flag. Restore it first. **Finding flags by owner.** The flags list has an **Owner** column and an owner filter with **Any owner**, **Mine**, **Unowned**, and each member by name. **Unowned** is the quickest way to find flags nobody has claimed yet. Owners, Admins and Members can set and clear owners. Viewers can’t. ## Setting an owner through the API The [Management API](/docs/management-api/overview/) takes the owner’s email address. It must belong to an active member of the organization. Name an owner when you create the flag with `ownerEmail`: ```plaintext POST /api/v1/orgs/{org}/projects/{project}/flags ``` ```json { "key": "checkout-redesign", "name": "Checkout Redesign", "type": "Boolean", "ownerEmail": "sam@example.com" } ``` Leave `ownerEmail` out and the owner is whoever the token belongs to. A service token creates the flag unowned. Set or change the owner of an existing flag: ```plaintext PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/owner ``` ```json { "email": "sam@example.com" } ``` Clear it: ```plaintext DELETE /api/v1/orgs/{org}/projects/{project}/flags/{flag}/owner ``` Both return `204 No Content`. An email that doesn’t match an active member of the organization is refused with `OWNER_NOT_MEMBER`, and an empty email is treated the same way, so use `DELETE` to clear. Flag responses carry the owner as an `owner` object with `id`, `email` and `name`, or `null` when nobody owns the flag. To list flags by owner, pass `?owner=` with an email address, or `?owner=none` for unowned flags: ```plaintext GET /api/v1/orgs/{org}/projects/{project}/flags?owner=none ``` The [API Reference](/docs/management-api/reference/) has the full request and response shapes. The [MCP server](/docs/integrations/mcp/) returns the `owner` field in `get_flag` and `list_flags` output, so an agent can tell you who owns a flag. `list_flags` and `find_stale_flags` also take an `owner` filter: an email, `me`, or `none` for the flags nobody owns. `set_flag_owner` assigns an owner by email, or `me` for whoever the token belongs to, and `create_flag` accepts `ownerEmail`. The plan rules below apply the same way they do in the API. ## When someone leaves the organization Removing a member from **Organization Settings** opens the **Remove Team Member** dialog. On Pro and above, if the person owns any flags, it says how many: “Owns 4 flags. They will become unowned unless you reassign them.” Pick a teammate under **Reassign their flags to** and every flag that person owned, archived ones included, moves to the teammate in the same step as the removal. Leave it on **Leave unowned** and the flags become unowned. Their cleanup notices then go to your Owners and Admins until someone claims them, and the **Unowned** filter shows exactly which flags those are. Only Owners and Admins can remove members, so only they make this choice. Each ownership change is recorded in the [audit log](/docs/guides/audit-log/) . A person who deletes their own account keeps their flags until the deletion completes, at the end of its grace period. From then on those flags are unowned. While the deletion is pending, cleanup notices for their flags go to your Owners and Admins instead of to them. ## Plan requirements Owners can be set and changed on **Pro**, **Business** and **Enterprise**. On Free and Team, the owner controls are hidden in the dashboard, and the API refuses a new owner with `PLAN_FEATURE_UNAVAILABLE`. Clearing an owner works on every plan. Free and Team still record an owner for you. Each new flag quietly stores its creator as owner, so when you upgrade to Pro, the flags your team already made arrive with owners, and nobody has to go back and fill them in. The API returns that stored `owner` on any plan. Removing a member on Free or Team leaves their flags unowned, because choosing who inherits them needs Pro. On Team, [cleanup notices](/docs/guides/cleanup-notices/) go to your Owners and Admins for every flag, whatever owner is stored. From Pro upward they go to the owner. Free has no cleanup notices. See [Plans and Limits](/docs/concepts/plans-and-limits/) for what each plan includes. ## Next steps - [Cleanup Notices](/docs/guides/cleanup-notices/) : the daily email that tells an owner a flag is ready to come out - [Flag Expiration Dates](/docs/guides/flag-expiration/) : give a flag the date its owner plans to remove it - [Team Management](/docs/guides/team-management/) : invite and remove members, and manage roles - [Stale Flag Detection](/docs/concepts/stale-flags/) : how Featureflip decides a flag has stopped doing anything --- ## Source: https://featureflip.io/docs/guides/prerequisites/ # How to Set Prerequisite Flags Prerequisites let you make one flag depend on another: a child flag stays off until a parent flag serves a specific variation. This guide walks through adding and managing prerequisites from the web dashboard. For the underlying model — evaluation order, variation matching, and guardrails — see [Prerequisite Flags](/docs/concepts/prerequisites/) . ## Where to find prerequisites - Open a flag from the **Flags** page at `/flags`. - On the flag detail page, select an **environment tab** (e.g., Development, Staging, Production). - The **Prerequisites** section appears beneath the targeting rules. Like targeting, prerequisites are configured per environment, so a flag can depend on a parent in production while running freely in development. ## Adding a prerequisite - In the **Prerequisites** section, click **Add Prerequisite**. - **Choose the parent flag** from the first dropdown — the flag this one should depend on. - **Choose the required variation** from the second dropdown — the variation the parent must serve for this flag to be considered on. The variation list re-populates when you change the parent, so it always reflects that parent’s current variations. - Click **Save**. You can add more than one prerequisite. They form an **AND** gate: every parent must serve its required variation before this flag’s targeting rules are evaluated. If any prerequisite is not met, the flag serves its **off variation** and no rules run. ## What happens when a prerequisite is not met When a parent does not serve the required variation, the dependent flag serves its off variation and the evaluation reason is set to `prerequisite-failed` (`PrerequisiteFailed` in client-side SDKs), with `prerequisiteKey` naming the parent that gated it off. You can confirm this without deploying: - Click **Evaluate** on the flag detail page. - Enter a JSON evaluation context. - The result shows the served variation, the reason, and the prerequisite key when a prerequisite gated the flag off. ## Removing a prerequisite Open the flag, find the row in the **Prerequisites** section, and remove it. The flag is then evaluated on its targeting rules alone. To stop being a parent for another flag, you remove the prerequisite from the **child** instead — a parent does not track its dependents, the dependency is declared on the child. ## Errors you might see Two cases are surfaced inline in the editor rather than as opaque API errors: - **Cycle rejected.** If a prerequisite would create a loop (for example `flag-a` requires `flag-b`, which already requires `flag-a`), the form shows the offending chain and refuses to save. - **Dependent blocks delete.** If you try to delete a flag, or remove a variation, that another flag depends on, the response lists the dependent flag keys. Remove those prerequisites first, then retry. ## Best practices - **Make the parent own the schedule.** Put the rollout percentage or targeting on the parent and let dependents follow it, instead of duplicating the same targeting on each child. - **Keep chains shallow.** One parent with several direct dependents is easier to reason about than a long chain. If you are approaching the depth cap of 10, flatten the structure. - **Name the dependency in the description.** A short note on the child flag (“gated on `new-checkout-v2 = on`”) saves the next person a lookup. - **Remove dependencies during cleanup.** A flag cannot be archived or deleted while others depend on it — unwire prerequisites as part of removing a flag. ## Next steps - [Prerequisite Flags](/docs/concepts/prerequisites/) — how prerequisites are evaluated, variation matching, and guardrails - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — control who sees which variation once prerequisites pass - [How to Create Feature Flags](/docs/guides/creating-flags/) — create the parent and child flags first - [Evaluate Endpoint](/docs/api/evaluate/) — reason strings and `prerequisiteKey` in the API response --- ## Source: https://featureflip.io/docs/guides/scheduled-changes/ # Scheduled Changes Scheduled changes let you queue a flag to turn on or off automatically at a future time, instead of logging in at the exact moment to flip it yourself. You set the target state and the time up front, and Featureflip applies the change on the server when it arrives — so a timed launch, an end-of-promotion cutoff, or a middle-of-the-night maintenance window becomes something you set once and forget. Scheduled changes are set **per flag, per environment**. A change you queue in Production has no effect on Staging. Note Scheduled changes are available on the Team, Pro, Business, and Enterprise plans. See [pricing](/pricing/) for details. ## Scheduling a change - Open the flag from your project’s **Flags** page. - Select the **environment tab** where you want the change to happen (for example, Production). - Click **Schedule…** next to the environment’s on/off control. - In the **Schedule on/off change** dialog: - **Target state** — choose **Turn ON** or **Turn OFF**. - **When** — pick the date and time, entered in your local time. The time must be in the future; Featureflip will not accept a moment that has already passed. - Click **Schedule**. Your change is now queued. It does nothing to the live flag until the time you chose arrives. ## Viewing pending and recent changes Each environment shows a **Scheduled changes** panel on the flag: - **Pending** — changes that are queued but have not applied yet. Each row shows the target state (Turn ON / Turn OFF), the time it will apply in your local time, and who scheduled it. - **Recent** — the last few changes that have already resolved, each with a status of **Applied**, **Cancelled**, or **Failed**. A failed change shows the reason it could not be applied. ## Cancelling a scheduled change You can cancel any change while it is still **Pending**: - Find the change in the **Pending** list of the flag’s Scheduled changes panel. - Click **Cancel** on that row. - Confirm in the dialog. The change moves to **Cancelled** and will never apply. To change the target state or timing of a pending change, cancel it and schedule a new one with the details you want. Once a change has already applied, it is part of the flag’s history and shows as **Applied** — it is not something you can undo from here. To reverse it, flip the flag again or schedule a new change. ## What happens when a change applies At the scheduled time, Featureflip re-runs the same flag change you would have made by hand. Because it goes through the normal change path rather than a separate one: - **It streams to your SDKs.** The new flag state propagates to every connected SDK over its streaming connection within seconds — no redeploy or restart. - **It is recorded in the audit log** under the person who scheduled it, so your flag [history](/docs/guides/audit-log/) shows who queued the change and when it took effect, not an anonymous system entry. - **The rest of evaluation is unchanged.** Scheduling sets *whether* a flag is on, not *how* it evaluates. When the flag turns on, your [targeting rules](/docs/guides/targeting-rules/) , [segments](/docs/guides/segments/) , rollout percentage, and [prerequisites](/docs/guides/prerequisites/) all still apply exactly as they would after a manual toggle. - **Failures are visible.** If something blocks the change when it is due, it is marked **Failed** with a reason rather than silently skipped. ## Common patterns - **Timed launch.** Schedule a flag to turn **ON** at your announcement time so the feature goes live exactly when planned, even if nobody is at a keyboard. - **Maintenance window.** Schedule one change to turn a flag **OFF** at the start of the window and a second change to turn it back **ON** at the end. Each is independent, and you can cancel or reschedule either side without touching the other. - **End-of-promotion cutoff.** Schedule a limited-time feature to turn **OFF** the moment the promotion ends. - **Off-hours cutover.** Set up a 2am flip from your desk during the working day and log off — the change runs on its own. ## Next steps - [Scheduled changes (feature overview)](/product/features/scheduled-changes/) — how scheduling fits with the rest of Featureflip - [Creating Feature Flags](/docs/guides/creating-flags/) — create and toggle the flags you want to schedule - [Feature Flag Kill Switch](/use-cases/kill-switch/) — turn a feature off fast, manually or on a schedule - [Audit Log](/docs/guides/audit-log/) — see who scheduled what, and when it applied --- ## Source: https://featureflip.io/docs/guides/segments/ # How to Use Segments for Targeting Segments are reusable groups of users defined by attribute conditions. Instead of duplicating the same targeting rules across multiple flags, define a segment once and reference it wherever you need it. ## Creating a segment - Navigate to the **Segments** page at `/segments`. - Click **Create Segment**. - Enter a **name** (e.g., “Beta Users”) and an optional **description**. - Add one or more **rules**. Each rule specifies: - An **attribute** from the evaluation context (e.g., `email`, `plan`, `country`). - An **operator** (equals, contains, in list, etc.). - One or more **values** to match. - Click **Save** to create the segment. Multiple rules within a segment are combined with **AND** logic. A user must match all rules to be included in the segment. ## Using segments in targeting When you create a targeting rule on a flag, you can target a segment instead of a single attribute: - Open a flag and select an environment tab. - Click **Add Rule** in the targeting panel. - Choose the **Segment** match type instead of selecting an individual attribute. - Select the segment you want to target. - Choose which variation to serve when the segment matches. - Save the rule. When a user’s evaluation context matches the segment’s rules, the targeting rule fires and serves the selected variation. ## Editing segments Open a segment from the Segments page and modify its rules. Changes to a segment’s rules **immediately affect all flags** that reference that segment. This is the key advantage of segments — update the definition in one place and every flag using it picks up the change. ## Deleting segments Before deleting a segment, remove it from all targeting rules that reference it. Deleting a segment that is still in use by a flag’s targeting rules will cause those rules to stop matching. - Check which flags reference the segment. - Update or remove those targeting rules. - Return to the Segments page and delete the segment. ## Example segments Here are common segments teams create: - **Internal Team** — Attribute `email` ends with `@yourcompany.com`. Use this to test features internally before rolling out to customers. - **Enterprise Customers** — Attribute `plan` in list `[enterprise, business]`. Target premium features to paying customers, the pattern behind [gating features by plan tier](/use-cases/entitlements/) . - **US Users** — Attribute `country` equals `US`. Roll out features by region. - **Beta Users** — Attribute `beta_enrolled` equals `true`. Let users opt in to early access features. ## Best practices - **Name segments clearly.** Use names that describe the group, not the feature (e.g., “Enterprise Customers” not “New Dashboard Users”). - **Keep segments focused.** Each segment should represent one logical group. Combine segments by using multiple targeting rules on a flag. - **Document segment purpose.** Use the description field to explain what the segment is for and who maintains it. ## Next steps - [Targeting & Segments](/docs/concepts/targeting/) — understand how segments fit into the evaluation model - [How to Set Up Targeting Rules](/docs/guides/targeting-rules/) — use segments in your targeting rules - [Rollout Strategies](/docs/concepts/rollout-strategies/) — combine segments with percentage rollouts --- ## Source: https://featureflip.io/docs/guides/single-sign-on/ # Single Sign-On (SSO) with Your Identity Provider Single sign-on lets your team open Featureflip through the identity provider (IdP) your company already runs, such as Okta or Microsoft Entra ID. People sign in with their work account, and your IdP decides who gets in. Featureflip connects over OpenID Connect (OIDC), so any provider that publishes an OIDC discovery document will work. SSO is on the **Business** plan and above. On other plans, the **Single sign-on** card shows “Single sign-on is available on the Business plan and above.” instead of the settings. Setup guides for specific providers: - [Okta](/docs/guides/sso-okta/) - [Microsoft Entra ID (Azure AD)](/docs/guides/sso-entra-id/) Both guides include a Terraform configuration for the IdP side, and the Entra guide has Azure CLI commands too, if you’d rather not click through an admin console. Everything below applies whichever provider you use. ## Before you start You need: - An organization on the Business or Enterprise plan. - The **Owner** role in that organization. Admins can view the connection, run the test and manage domains, but only Owners can save the connection, activate it or require SSO. - Admin access to your IdP, so you can register a new application there. - Access to DNS for the email domain your team signs in with. Everything lives in **Organization Settings**, in the **Single sign-on** card at the bottom of the page. The card is visible to Admins and Owners. ## How setup works - **Verify your email domain.** Publish a DNS TXT record so Featureflip knows the domain is yours. - **Register Featureflip in your IdP.** Create an OIDC web application and paste in Featureflip’s redirect URI. - **Save the connection.** Enter the issuer URL, client ID and client secret from your IdP. - **Test the connection.** Sign in through your IdP once to prove the round trip works. - **Activate it.** Your team can now use **Continue with SSO** on the sign-in page. - **Require SSO (optional).** Members must then come through your IdP to open the organization. The redirect URI to register in your IdP is the same for every organization: ```plaintext https://api.featureflip.io/api/management/v1/sso/oidc/callback ``` The card also shows it under **Redirect URI (paste into your IdP)** with a **Copy** button. ## Verify your email domains A connection only admits people whose email address is on a domain your organization has verified. That rule applies on every sign-in, including people who have signed in before. It covers the email on their Featureflip account too: if someone changes their account email to an address off your verified domains, SSO stops signing them in until it’s back on one. When you remove a domain, any account created on it that has no password moves to the address your IdP sends at that member’s next SSO sign-in. The IdP has to confirm that address, and no other Featureflip account can already hold it. If another organization has verified the old domain by then, the account stays where it is and SSO refuses the sign-in. - In the **Domains** section, type your domain (for example `example.com`) into **Add a domain** and click **Add domain**. - Featureflip shows a **TXT name** and a **TXT value**. Create that record at your DNS provider. The name looks like `_featureflip-verification.example.com` and the value like `featureflip-verification=` followed by 32 characters. - Click **Verify**. The badge changes from **Pending** to **Verified** once the record is visible. In a zone file, the record looks like this (use the value from your own card): ```txt _featureflip-verification.example.com. 300 IN TXT "featureflip-verification=735e13cf55d5be72ad915a93d2eb14d1" ``` Before you click **Verify**, you can check that the record is live from any terminal. An empty result means it hasn’t propagated yet. ```bash dig +short TXT _featureflip-verification.example.com # On Windows: nslookup -type=TXT _featureflip-verification.example.com ``` DNS changes can take a few minutes to appear, so if **Verify** reports that no record was found, wait and try again. You can list up to 20 domains, and a domain can be verified by one organization at a time. Leave the record in place. Featureflip re-checks every verified domain once a day. If the record disappears, the domain is marked **Record missing** and every Owner gets an email, but sign-in keeps working while you fix it. Put the record back and click **Verify** to clear the badge right away. Otherwise the next daily check clears it. Owners get a reminder a week before the 30-day mark, and if the record is still missing when that day arrives, the domain is released. From then on, single sign-on stops covering addresses on it and any other organization can verify it. If it was your last verified domain, **Require single sign-on** switches off as well, so members can still sign in with a password or social login. Want it back? Restore the record and click **Verify**. ## Save and test the connection After you have registered the app in your IdP (see the [Okta](/docs/guides/sso-okta/) or [Entra ID](/docs/guides/sso-entra-id/) guide), fill in the **Identity provider** section: | Field | What to enter | | --- | --- | | Display name | Any label, for example “Acme Okta”. | | Issuer URL | Your IdP’s OIDC issuer. It must be an https URL and must match what your IdP reports about itself exactly. | | Client ID | The client ID of the app you registered. | | Client secret | The secret for that app. Featureflip stores it encrypted and never shows it again. When you edit the connection later, leave the field blank to keep the current secret. | | Secret expires on | Optional. If you enter the date your IdP gave the secret, Owners get an email 30 days and 7 days before it expires. | Click **Save**. Featureflip loads your issuer’s discovery document straight away, so a wrong issuer is caught here rather than at sign-in. If the provider identifies itself with a slightly different issuer (a trailing slash, say), the error message quotes the exact value to use. You can read that value yourself before saving. Every OIDC provider publishes a discovery document at `/.well-known/openid-configuration` under its issuer, and the `issuer` field in it is exactly what Featureflip compares against: ```bash curl -s https://acme.okta.com/.well-known/openid-configuration | jq -r .issuer # https://acme.okta.com ``` If that command fails, Featureflip can’t reach the document either. The same response lists `scopes_supported` and `claims_supported`, which should include `openid`, `email` and `profile`. Once saved, the card also shows an **Initiate login URI (for your IdP’s app tile)**. Add it to your IdP if you want people to launch Featureflip from their app dashboard. Then click **Test connection**. Your IdP asks you to sign in and hands you back to Organization Settings with the result. The test doesn’t create an account or a session. Your own email needs to be on a verified domain for it to pass, so verify the domain first. Your IdP also has to confirm that email, as described under Other OIDC providers . A test link is good for 10 minutes. ## Activate the connection Click **Activate**. An Owner can activate once a test has passed since the issuer, client ID or secret last changed. From then on, anyone on a verified domain can click **Continue with SSO** on the [sign-in page](https://app.featureflip.io/login) , enter their **Work email**, and continue to your IdP. Editing the issuer or client ID later puts the connection back into Draft, turns SSO enforcement off and signs out everyone who came in through SSO. Test and activate again afterwards. The edit also disconnects everyone’s account from the old IdP, so you can move to a new IdP or app registration without locking anyone out: each person’s next SSO sign-in connects their existing account to the new one, as long as the new IdP confirms their email address. Changing only the secret keeps the connection active, enforcement on and everyone signed in, though you should run the test again to confirm the new secret works. Renaming the connection or changing the expiry date doesn’t do any of that. **Deactivate** has the same effect as those edits: it stops SSO sign-in, turns enforcement off and ends SSO sessions. Personal access tokens created in those sessions keep working, but they no longer count as SSO sign-in. Once SSO is required again, they’re refused with [`sso_required`](/docs/management-api/errors/sso_required/) and need replacing. ## Choose who can join The **Access policy** section controls what happens when someone signs in through your IdP for the first time. **Add new people automatically** is on by default. Anyone your IdP lets in, with an email on a verified domain, joins your organization with the **Default role** you pick: Viewer, Member or Admin. Owner can never be the default. People who join this way have no Featureflip password and show an **SSO** badge in the member list. Any of them can add a password later. On the [sign-in page](https://app.featureflip.io/login) they click **Forgot password?** and enter their work email, and Featureflip emails them a **Set a password** link that lets them choose one. With a password they can still get into Featureflip if your IdP is down, or if your organization moves off the Business plan. It doesn’t get them past **Require single sign-on**, though. While that’s on, they still come through your IdP to open your organization. If they haven’t verified their email address yet, setting a password also disconnects their account from your IdP. Whoever redeems the link has proved they own the mailbox, and an unconfirmed SSO sign-in hasn’t. Once they verify the address, their next SSO sign-in connects it again. With it off, only people you have invited can join. Their invite is accepted when they first sign in, and the role on the invite applies. That only happens when your IdP confirms their email address, as described under Other OIDC providers . A few rules hold either way: - Each person uses a seat. If your plan’s member limit is reached, the sign-in is refused until you free a seat or upgrade. - Someone you removed from the organization is never added back automatically. They need a new invite. - If someone already has a Featureflip account with the same email, their first SSO sign-in links to it, as long as your IdP confirms the address and the Featureflip account’s email is verified. To decide who can reach Featureflip at all, use your IdP’s assignments. The provider guides show where. ## Require SSO Turn on **Require single sign-on** to make your IdP the only way into the organization. The switch is available once the connection is active, and only Owners can change it. With SSO required: - Members who sign in with a password or with Google or GitHub can’t open the organization. They are asked to use SSO. Their other organizations are unaffected. - **Owners keep a recovery path.** An Owner can still sign in with their Featureflip password, or with Google or GitHub if that’s how they signed up, so a misconfigured IdP can never lock everyone out. Each of those sign-ins is recorded in the [audit log](/docs/guides/audit-log/) . The Owner role is never handed out through SSO, so whoever holds it already had a way in before you connected your IdP. - A verified domain can’t be removed until enforcement is off again. **Deactivate** also turns it off, and it’s the only way to do that after a downgrade or while the organization is deactivated, because the switch isn’t shown then. Enforce multi-factor authentication in your IdP. Featureflip asks for its own two-factor code only from people who turned it on for their account, and they enter it after your IdP lets them in. ## Sessions, offboarding and API tokens An SSO session lasts up to 24 hours. Then Featureflip quietly routes the person back through your IdP. They usually don’t notice, and they land on the page they were on. Each renewal also checks that your connection is still active. If it has been deactivated or deleted, or your organization has moved below Business, the person is signed out and sees the normal sign-in page instead. Removing a user at your IdP ends their sessions within 24 hours. Also remove them from the organization in Featureflip to cut off their API tokens. Personal access tokens outlive a browser session. While SSO is required, only tokens created during an SSO session for your organization are accepted. A token that isn’t gets a `403` with the [`sso_required`](/docs/management-api/errors/sso_required/) error. Removing the member in [Team Management](/docs/guides/team-management/) revokes their access at once, tokens included. ## If your organization moves below Business SSO follows the plan. On Free, Team or Pro: - **Continue with SSO** stops working for your organization, and people see “Single sign-on isn’t active for this organization.” - **Require single sign-on** stops being enforced, so members can open the organization with a password, Google or GitHub again. Anyone who only ever signed in through SSO should set a password with **Forgot password?** first, as described in Choose who can join . - Your connection, verified domains and access policy are kept as they are. Back on Business or Enterprise, SSO works again without setting it up from scratch. An Owner can still **Deactivate** the connection after a downgrade. If SSO was required, deactivating is also what lets you remove a verified domain. Activating it again needs the Business plan or above. [Plans and Limits](/docs/concepts/plans-and-limits/) covers what else changes when you switch plans. ## If your organization is deactivated Members who already belong to the organization can still sign in with SSO. They land on the same deactivated notice a password sign-in shows, and an Owner can reactivate from there. Nobody new can join through SSO while the organization is deactivated. A sign-in that would add a member stops with “This organization is deactivated, so it can’t add new members.” The notice also lets an Owner **Deactivate** or **Delete** the connection, and lets an Owner or Admin remove domains, without reactivating first. If SSO was required, an Owner has to deactivate the connection before a verified domain can go. ## Other OIDC providers Beyond Okta and Entra ID, the same steps work for any provider with a standard OIDC discovery document. Register a confidential web application with the authorization code grant, allow the `openid`, `email` and `profile` scopes, and use the issuer your provider publishes. Two things are checked for every provider: - The ID token must carry an `email` claim. An `email_verified` claim set to `false` is refused. - The issuer must belong to your tenant. Shared multi-tenant endpoints, such as Entra ID’s `/common` or `/organizations`, are refused. With Google Workspace as the provider, the account’s Workspace domain must be one you have verified. Featureflip also needs the IdP to confirm the address by sending `email_verified` set to `true`. Entra ID never sends that claim. It uses `xms_edov`, which the [Entra ID guide](/docs/guides/sso-entra-id/) adds in step 4. A sign-in the IdP hasn’t confirmed can’t link to an existing account or accept an invite, so **Test connection** fails until the claim arrives. If your IdP still leaves it off for someone, **Add new people automatically** lets them in at the **Default role**. Featureflip then emails them a link, and they can’t use their account until they click it and verify the address. ## Troubleshooting These are the messages you are most likely to see, and what to check. | Message | What to check | | --- | --- | | ”The provider identifies itself as ”…”. Use that exact value as the issuer.” | Copy the quoted value into Issuer URL as-is. | | ”Couldn’t load … Check the issuer URL.” | The issuer is wrong, or its /.well-known/openid-configuration document isn’t reachable from the internet. | | ”Your identity provider rejected the test sign-in. Check the client ID and secret.” | The client ID or secret doesn’t match the app, or the secret has expired. | | ”The test sign-in worked, but that account’s email isn’t on a verified domain.” | Verify the domain of the account you tested with. | | ”The test expired or was opened in another browser. Run it again.” | Start the test again and finish it within 10 minutes in the same browser. | | ”Single sign-on isn’t set up for that email domain.” | Shown on the sign-in page. The domain isn’t verified by any organization with an active connection. | | ”This organization has no free seats.” | Free a seat in Team Management or move to a larger plan. | | ”You were removed from this organization. Ask an admin to invite you again.” | Send that person a new invite. | | ”The test sign-in worked, but your identity provider didn’t confirm that account’s email. …” | Your IdP isn’t sending email_verified: true, or xms_edov on Entra ID. See Other OIDC providers. | | ”Your Featureflip account’s email is no longer on this organization’s domain, …” | The person changed their account email to an address off your verified domains. They can sign in with a password and change it back, or you can verify the new domain. A member with no password gets back in when your IdP sends a confirmed address, on a verified domain, that no other Featureflip account uses. SSO can’t move the account once another organization has verified their old domain. | | ”Verify the email address on your existing Featureflip account, then try single sign-on again.” | The person has an older Featureflip account with an unverified email. Verifying it lets SSO link to it. | | ”Your identity provider didn’t confirm your email address, so we can’t use it to sign in to an existing account or accept an invitation.” | Your IdP isn’t sending email_verified: true, or xms_edov on Entra ID. See Other OIDC providers. | --- ## Source: https://featureflip.io/docs/guides/sso-entra-id/ # Set Up SSO with Microsoft Entra ID (Azure AD) This guide connects your Microsoft Entra ID tenant to Featureflip so your team signs in with their Microsoft work account. Entra ID was called Azure Active Directory (Azure AD) until 2023, and the steps are the same whichever name your admin center shows. Plan on about twenty minutes, most of it in the Entra admin center. SSO is on the **Business** plan and above. The [single sign-on overview](/docs/guides/single-sign-on/) explains the settings that are the same for every provider, such as the access policy and what requiring SSO does. You need: - An Entra account that can register applications (Application Developer or above) and, for step 4, grant admin consent (Cloud Application Administrator or above). - The **Owner** role in your Featureflip organization. - DNS access for your team’s email domain. ## 1. Verify your email domain in Featureflip Do this first, because the connection test only passes for an email on a verified domain. - In Featureflip, open **Organization Settings** and scroll to the **Single sign-on** card. - Under **Domains**, enter your domain in **Add a domain** and click **Add domain**. - Create the DNS TXT record shown (**TXT name** and **TXT value**) at your DNS provider. - Click **Verify**. The badge turns **Verified** once the record is visible, which can take a few minutes. To see whether the record has propagated before you click **Verify**: ```bash dig +short TXT _featureflip-verification.example.com ``` Keep the record in place afterwards. Featureflip re-checks it daily. ## 2. Register the app in Entra ID Steps 2, 3, 4 and 7 can also be done from code. See Set up Entra ID with Terraform or the Azure CLI , then pick up again at step 5. - Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com) . - Go to **Entra ID** > **App registrations** and select **New registration**. - Set **Name** to `Featureflip`. - Under **Supported account types**, choose the single-tenant option (**Single tenant only**, or **Accounts in this organizational directory only** in older versions of the admin center). Featureflip refuses multi-tenant sign-in, so the other options won’t work. - Under **Redirect URI**, choose the **Web** platform and enter: ```plaintext https://api.featureflip.io/api/management/v1/sso/oidc/callback ``` The same value is in Featureflip under **Redirect URI (paste into your IdP)**, with a **Copy** button. - Select **Register**. On the app’s **Overview** page, copy the **Application (client) ID** and the **Directory (tenant) ID**. You need both. ## 3. Create a client secret - In the app, open **Certificates & secrets** > **Client secrets** and select **New client secret**. - Add a description, such as `Featureflip SSO`, and pick an expiry. Entra secrets always expire, after two years at most. - Select **Add**, then copy the **Value** column right away. Entra only shows it once. The **Secret ID** next to it is a different thing and won’t work. Write down the expiry date. You’ll enter it in Featureflip in step 5 so you get a reminder before it lapses. Once a secret expires, nobody can sign in with SSO until a new one is saved. ## 4. Allow the sign-in scopes Featureflip asks for the `openid`, `email` and `profile` scopes. Granting consent once, as an admin, means your team isn’t prompted on their first sign-in, and it’s required if your tenant blocks user consent. - Open **API permissions** and select **Add a permission** > **Microsoft Graph** > **Delegated permissions**. - Check **email**, **openid** and **profile**, then select **Add permissions**. - Select **Grant admin consent for** your tenant, and confirm. Next, open **Token configuration**, select **Add optional claim**, choose the **ID** token type and add **email** and **xms_edov**. The first puts the email address in the sign-in token. The second tells Featureflip the address is on a domain your tenant has verified, and Entra only sends it alongside **email**. Don’t skip `xms_edov`. Without it a first SSO sign-in can’t connect to someone’s existing Featureflip account or accept an invitation sent to them, so **Test connection** in step 6 won’t pass. ## 5. Save the connection in Featureflip Your issuer is built from the Directory (tenant) ID you copied in step 2: ```plaintext https://login.microsoftonline.com//v2.0 ``` /v2.0"> Use the tenant ID itself, the long value made of letters, digits and dashes, and keep `/v2.0` at the end. Entra’s shared endpoints (`/common`, `/organizations` and `/consumers`) are refused because they would accept accounts from any tenant. If you only know your domain, Entra will translate it. Ask for the discovery document with a domain verified in your tenant where the tenant ID would go, and the answer carries the tenant-specific issuer: ```bash curl -s https://login.microsoftonline.com/example.com/v2.0/.well-known/openid-configuration | jq -r .issuer # https://login.microsoftonline.com/9f3c2a71-4b8e-4d25-a6f0-2c7e1b5d8a43/v2.0 ``` With the Azure CLI signed in to the right tenant, `az account show --query tenantId -o tsv` prints the tenant ID on its own. Back in the **Single sign-on** card, fill in **Identity provider**: | Field | Value | | --- | --- | | Display name | For example, Entra ID | | Issuer URL | The issuer above | | Client ID | The Application (client) ID | | Client secret | The secret Value from step 3 | | Secret expires on | The expiry date from step 3. Owners get an email 30 days and 7 days before it. | Click **Save**. If the issuer is off, for instance with your domain name where the tenant ID should be, the error message quotes the exact value Entra reports. Copy that in and save again. The card now shows an **Initiate login URI (for your IdP’s app tile)**. You’ll use it in step 7. ## 6. Test and activate - In Featureflip, click **Test connection**. After a Microsoft sign-in you land back in Organization Settings. A passing test shows “Connection test passed. You can activate single sign-on.” - Click **Activate**. The account you test with needs an email on your verified domain. The test doesn’t create an account or a session. Your team can now click **Continue with SSO** on the Featureflip sign-in page and enter their work email. ## 7. Limit who can sign in and add the My Apps tile By default, every account in your tenant can sign in to a newly registered app. To restrict Featureflip to chosen people: - Go to **Entra ID** > **Enterprise apps** and open **Featureflip**. - Under **Properties**, set **Assignment required?** to **Yes** and save. - Under **Users and groups**, add the users or groups that should have access. To show Featureflip in your team’s My Apps portal, set **Visible to users?** to **Yes** on the same **Properties** page. Then, in the app registration, open **Branding & properties** and paste the **Initiate login URI** from Featureflip into **Home page URL**. The tile then opens a Featureflip page with a **Continue** button, and clicking that starts the same sign-in as **Continue with SSO**. ## 8. Choose the access policy and, optionally, require SSO Under **Access policy**: - **Add new people automatically** lets anyone Entra admits, with an email on a verified domain, join Featureflip with the **Default role** you choose. Turn it off if you’d rather invite people one by one. - **Require single sign-on** makes Entra ID the only way in. The overview explains the [Owner recovery path](/docs/guides/single-sign-on/#require-sso) that stays open. Require MFA through an Entra Conditional Access policy. Featureflip adds its own two-factor step only for people who turned it on for their account. ## Set up Entra ID with Terraform or the Azure CLI Both versions below register the app, create the client secret, grant admin consent for `openid`, `email` and `profile`, and turn on **Assignment required?** so only the people you assign can sign in. Afterward, carry on from step 5 . ### Terraform This uses the [AzureAD provider](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs) and limits access to one group. Replace `Engineering` with your own group, and set `end_date` to the secret expiry you want (two years at most). ```hcl terraform { required_providers { azuread = { source = "hashicorp/azuread", version = "~> 3.0" } } } variable "featureflip_initiate_login_uri" { description = "Initiate login URI from Featureflip's Single sign-on card. Leave null until the connection is saved." type = string default = null } data "azuread_client_config" "current" {} data "azuread_application_published_app_ids" "well_known" {} data "azuread_group" "featureflip_users" { display_name = "Engineering" } resource "azuread_service_principal" "msgraph" { client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] use_existing = true } resource "azuread_application" "featureflip" { display_name = "Featureflip" sign_in_audience = "AzureADMyOrg" web { redirect_uris = ["https://api.featureflip.io/api/management/v1/sso/oidc/callback"] homepage_url = var.featureflip_initiate_login_uri } required_resource_access { resource_app_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] dynamic "resource_access" { for_each = ["openid", "email", "profile"] content { id = azuread_service_principal.msgraph.oauth2_permission_scope_ids[resource_access.value] type = "Scope" } } } optional_claims { id_token { name = "email" } # Confirms the email is on a domain your tenant verified. Needed to link existing # Featureflip accounts and accept invitations. id_token { name = "xms_edov" } } } resource "azuread_application_password" "featureflip" { application_id = azuread_application.featureflip.id display_name = "Featureflip SSO" end_date = "2028-09-26T00:00:00Z" } resource "azuread_service_principal" "featureflip" { client_id = azuread_application.featureflip.client_id app_role_assignment_required = true } # Admin consent for openid, email and profile, so nobody sees a consent prompt. resource "azuread_service_principal_delegated_permission_grant" "featureflip" { service_principal_object_id = azuread_service_principal.featureflip.object_id resource_service_principal_object_id = azuread_service_principal.msgraph.object_id claim_values = ["openid", "email", "profile"] } # Only members of this group can sign in (Assignment required? = Yes above). resource "azuread_app_role_assignment" "featureflip_users" { app_role_id = "00000000-0000-0000-0000-000000000000" principal_object_id = data.azuread_group.featureflip_users.object_id resource_object_id = azuread_service_principal.featureflip.object_id } output "issuer" { value = "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0" } output "client_id" { value = azuread_application.featureflip.client_id } output "client_secret" { value = azuread_application_password.featureflip.value sensitive = true } output "client_secret_expires_on" { value = azuread_application_password.featureflip.end_date } ``` 3.0" } }}variable "featureflip_initiate_login_uri" { description = "Initiate login URI from Featureflip's Single sign-on card. Leave null until the connection is saved." type = string default = null}data "azuread_client_config" "current" {}data "azuread_application_published_app_ids" "well_known" {}data "azuread_group" "featureflip_users" { display_name = "Engineering"}resource "azuread_service_principal" "msgraph" { client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] use_existing = true}resource "azuread_application" "featureflip" { display_name = "Featureflip" sign_in_audience = "AzureADMyOrg" web { redirect_uris = ["https://api.featureflip.io/api/management/v1/sso/oidc/callback"] homepage_url = var.featureflip_initiate_login_uri } required_resource_access { resource_app_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"] dynamic "resource_access" { for_each = ["openid", "email", "profile"] content { id = azuread_service_principal.msgraph.oauth2_permission_scope_ids[resource_access.value] type = "Scope" } } } optional_claims { id_token { name = "email" } # Confirms the email is on a domain your tenant verified. Needed to link existing # Featureflip accounts and accept invitations. id_token { name = "xms_edov" } }}resource "azuread_application_password" "featureflip" { application_id = azuread_application.featureflip.id display_name = "Featureflip SSO" end_date = "2028-09-26T00:00:00Z"}resource "azuread_service_principal" "featureflip" { client_id = azuread_application.featureflip.client_id app_role_assignment_required = true}# Admin consent for openid, email and profile, so nobody sees a consent prompt.resource "azuread_service_principal_delegated_permission_grant" "featureflip" { service_principal_object_id = azuread_service_principal.featureflip.object_id resource_service_principal_object_id = azuread_service_principal.msgraph.object_id claim_values = ["openid", "email", "profile"]}# Only members of this group can sign in (Assignment required? = Yes above).resource "azuread_app_role_assignment" "featureflip_users" { app_role_id = "00000000-0000-0000-0000-000000000000" principal_object_id = data.azuread_group.featureflip_users.object_id resource_object_id = azuread_service_principal.featureflip.object_id}output "issuer" { value = "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/v2.0"}output "client_id" { value = azuread_application.featureflip.client_id}output "client_secret" { value = azuread_application_password.featureflip.value sensitive = true}output "client_secret_expires_on" { value = azuread_application_password.featureflip.end_date}"> After `terraform apply`, the outputs hold everything step 5 asks for: `terraform output issuer`, `terraform output client_id`, `terraform output -raw client_secret` and `terraform output client_secret_expires_on`. Once the connection is saved, apply again with `-var 'featureflip_initiate_login_uri=…'` to add the My Apps tile. To show that tile, also set **Visible to users?** to **Yes** as described in step 7. The client secret sits in your Terraform state in plain text, so keep that state somewhere encrypted. ### Azure CLI Sign in with `az login` as someone who can grant admin consent, then run: ```bash REDIRECT_URI=https://api.featureflip.io/api/management/v1/sso/oidc/callback GRAPH=00000003-0000-0000-c000-000000000000 # Microsoft Graph # Step 2: register a single-tenant web app APP_ID=$(az ad app create \ --display-name Featureflip \ --sign-in-audience AzureADMyOrg \ --web-redirect-uris "$REDIRECT_URI" \ --optional-claims '{"idToken":[{"name":"email"},{"name":"xms_edov"}]}' \ --query appId -o tsv) # Step 3: a client secret that expires in two years (printed once, so store it now) az ad app credential reset --id "$APP_ID" \ --display-name "Featureflip SSO" --years 2 --append \ --query password -o tsv # Step 4: openid, email and profile, with admin consent az ad app permission add --id "$APP_ID" --api "$GRAPH" --api-permissions \ 37f7f235-527c-4136-accd-4a02d197296e=Scope \ 64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0=Scope \ 14dad69e-099b-42c9-810b-d002981feec1=Scope az ad sp create --id "$APP_ID" az ad app permission admin-consent --id "$APP_ID" # Step 7: only assigned users and groups can sign in az ad sp update --id "$APP_ID" --set appRoleAssignmentRequired=true # Values for step 5 echo "Issuer: https://login.microsoftonline.com/$(az account show --query tenantId -o tsv)/v2.0" echo "Client ID: $APP_ID" ``` The three GUIDs are Microsoft Graph’s `openid`, `email` and `profile` delegated permissions, which have the same IDs in every tenant. Assign users or groups afterward under **Enterprise apps** > **Featureflip** > **Users and groups**, as in step 7. ## Offboarding Removing someone’s assignment, or disabling their account in Entra, stops them signing in again. Removing a user at your IdP ends their sessions within 24 hours. Also remove them from the organization in Featureflip to cut off their API tokens. ## Troubleshooting **“Use your tenant-specific issuer (https://login.microsoftonline.com//v2.0), not a multi-tenant endpoint.”** The issuer points at `/common` or `/organizations`. Replace that segment with your Directory (tenant) ID. **“The provider identifies itself as ”…”. Use that exact value as the issuer.”** Copy the quoted issuer into **Issuer URL**. This usually means a domain name was used in place of the tenant ID, or `/v2.0` is missing. **“Your identity provider rejected the test sign-in. Check the client ID and secret.”** Check that you pasted the secret’s **Value** and not its **Secret ID**, and that the secret hasn’t expired. **Microsoft shows a redirect URI mismatch error (AADSTS50011).** Under **Authentication**, the **Web** redirect URI must match the Featureflip value exactly. **Microsoft says the user isn’t assigned to the app (AADSTS50105).** With **Assignment required?** on, add the user or one of their groups under **Users and groups**. **“The test sign-in worked, but that account’s email isn’t on a verified domain.”** Featureflip uses the account’s email address, or its user principal name when the account has no email. Whichever it is must be on a domain you verified. Accounts whose sign-in name is on `onmicrosoft.com` need an email address on your own domain. **“Your identity provider didn’t confirm your email address, so we can’t use it to sign in to an existing account or accept an invitation.”** (or, from **Test connection**, “your identity provider didn’t confirm that account’s email”) The ID token is missing `email` or `xms_edov`, or `xms_edov` is `false`. Add both under **Token configuration** as in step 4. An account with no email address fails too, because its user principal name can’t be confirmed this way. It’s only `true` when the address is on a domain verified in your tenant, so check **Domain names** in Entra if it still fails. The [single sign-on overview](/docs/guides/single-sign-on/#troubleshooting) lists the remaining messages. --- ## Source: https://featureflip.io/docs/guides/sso-okta/ # Set Up SSO with Okta (OIDC) With Okta connected, your team opens Featureflip with the same Okta account they use for everything else at work. Setup takes about fifteen minutes and needs one new app integration in Okta. SSO is on the **Business** plan and above. The [single sign-on overview](/docs/guides/single-sign-on/) explains the settings that are the same for every provider, such as the access policy and what requiring SSO does. You need: - An Okta account with permission to create app integrations (Super Administrator or Application Administrator). - The **Owner** role in your Featureflip organization. - DNS access for your team’s email domain. ## 1. Verify your email domain in Featureflip Do this first, because the connection test only passes for an email on a verified domain. - In Featureflip, open **Organization Settings** and scroll to the **Single sign-on** card. - Under **Domains**, enter your domain in **Add a domain** and click **Add domain**. - Create the DNS TXT record shown (**TXT name** and **TXT value**) at your DNS provider. - Click **Verify**. The badge turns **Verified** once the record is visible, which can take a few minutes. To see whether the record has propagated before you click **Verify**: ```bash dig +short TXT _featureflip-verification.example.com ``` Keep the record in place afterwards. Featureflip re-checks it daily. ## 2. Create the app integration in Okta If you manage Okta with Terraform, Set up Okta with Terraform covers this step and step 5. Then come back for step 3. - In the Okta Admin Console, go to **Applications** > **Applications** and click **Create App Integration**. - For **Sign-in method**, choose **OIDC - OpenID Connect**. For **Application type**, choose **Web Application**. Click **Next**. - Set **App integration name** to `Featureflip`. - Under **Grant type**, keep **Authorization code** selected. Featureflip doesn’t need the others. - In **Sign-in redirect URIs**, remove the example and add: ```plaintext https://api.featureflip.io/api/management/v1/sso/oidc/callback ``` The same value is in Featureflip under **Redirect URI (paste into your IdP)**, with a **Copy** button. - Leave **Sign-out redirect URIs** empty. - Under **Assignments**, choose who can use Featureflip. **Limit access to selected groups** is the usual choice. Pick the group that should have access. - Click **Save**. Okta opens the new app’s **General** tab. Under **Client Credentials**, copy the **Client ID**. Under **Client Secrets**, copy the secret. Keep both for the next step. Client authentication should be **Client secret**, which is the default for a web application. ## 3. Find your issuer URL For most Okta orgs the issuer is your Okta domain, with nothing after it: ```plaintext https://acme.okta.com ``` Use your own domain in place of `acme.okta.com`. If you sign in to Okta through a custom domain such as `login.acme.com`, the issuer uses that domain instead. If your team uses a custom authorization server (under **Security** > **API**), its issuer includes a path, such as `https://acme.okta.com/oauth2/default`. That works too. Use the **Issuer URI** Okta shows for that server. Either way, you don’t have to get it perfect. When you save, Featureflip reads Okta’s discovery document, and if the value you typed differs from what Okta reports, the error message quotes the exact issuer to use. You can also ask Okta directly. The `issuer` field of the discovery document is the value to paste: ```bash # Org authorization server curl -s https://acme.okta.com/.well-known/openid-configuration | jq -r .issuer # Custom authorization server curl -s https://acme.okta.com/oauth2/default/.well-known/openid-configuration | jq -r .issuer ``` ## 4. Save the connection in Featureflip Back in the **Single sign-on** card, fill in **Identity provider**: | Field | Value | | --- | --- | | Display name | For example, Okta | | Issuer URL | The issuer from step 3 | | Client ID | The Client ID from Okta | | Client secret | The secret from Okta | | Secret expires on | Leave empty unless your team sets secrets to expire. Okta’s client secrets don’t expire unless you rotate them. | Click **Save**. The card now shows an **Initiate login URI (for your IdP’s app tile)**. ## 5. Add the Featureflip tile to Okta (optional) To let people open Featureflip from their Okta End-User Dashboard: - In Okta, open the Featureflip app and edit **General Settings**. - Set **Login initiated by** to **Either Okta or App**. - Paste the **Initiate login URI** from Featureflip into **Initiate login URI**. - Turn on displaying the application icon to users, then save. Clicking the tile opens a Featureflip page with a **Continue** button, and clicking that starts the same sign-in as **Continue with SSO**. The extra click is on purpose: it stops another website from starting a sign-in for someone without their knowledge. ## 6. Test and activate - In Featureflip, click **Test connection**. Okta asks you to sign in, then sends you back to Organization Settings. A passing test shows “Connection test passed. You can activate single sign-on.” - Click **Activate**. The Okta account you test with must be assigned to the app and have an email on your verified domain. The test doesn’t create an account or a session. Your team can now click **Continue with SSO** on the Featureflip sign-in page and enter their work email. ## 7. Choose the access policy and, optionally, require SSO Under **Access policy**: - **Add new people automatically** lets anyone assigned to the app in Okta join Featureflip with the **Default role** you choose. Turn it off if you’d rather invite people one by one. - **Require single sign-on** makes Okta the only way into your organization. Owners can still sign in with their Featureflip password, as a recovery path that’s written to the [audit log](/docs/guides/audit-log/) . MFA belongs in Okta once SSO is on, and an Okta authentication policy is the place to require it. Featureflip’s own two-factor prompt only appears for people who turned it on for their account. ## Set up Okta with Terraform The [Okta Terraform provider](https://registry.terraform.io/providers/okta/okta/latest/docs) can create the app integration from step 2, assign it to a group, and add the dashboard tile from step 5. Replace `Engineering` with the group that should have access. ```hcl terraform { required_providers { okta = { source = "okta/okta", version = "~> 7.0" } } } variable "featureflip_initiate_login_uri" { description = "Initiate login URI from Featureflip's Single sign-on card. Leave null until the connection is saved." type = string default = null } data "okta_group" "featureflip_users" { name = "Engineering" } resource "okta_app_oauth" "featureflip" { label = "Featureflip" type = "web" grant_types = ["authorization_code"] response_types = ["code"] token_endpoint_auth_method = "client_secret_basic" redirect_uris = ["https://api.featureflip.io/api/management/v1/sso/oidc/callback"] # The Okta dashboard tile. Needs the URI Featureflip shows after you save the connection. login_mode = var.featureflip_initiate_login_uri == null ? "DISABLED" : "SPEC" login_uri = var.featureflip_initiate_login_uri hide_web = var.featureflip_initiate_login_uri == null hide_ios = true } resource "okta_app_group_assignments" "featureflip" { app_id = okta_app_oauth.featureflip.id group { id = data.okta_group.featureflip_users.id } } output "client_id" { value = okta_app_oauth.featureflip.client_id } output "client_secret" { value = okta_app_oauth.featureflip.client_secret sensitive = true } ``` 7.0" } }}variable "featureflip_initiate_login_uri" { description = "Initiate login URI from Featureflip's Single sign-on card. Leave null until the connection is saved." type = string default = null}data "okta_group" "featureflip_users" { name = "Engineering"}resource "okta_app_oauth" "featureflip" { label = "Featureflip" type = "web" grant_types = ["authorization_code"] response_types = ["code"] token_endpoint_auth_method = "client_secret_basic" redirect_uris = ["https://api.featureflip.io/api/management/v1/sso/oidc/callback"] # The Okta dashboard tile. Needs the URI Featureflip shows after you save the connection. login_mode = var.featureflip_initiate_login_uri == null ? "DISABLED" : "SPEC" login_uri = var.featureflip_initiate_login_uri hide_web = var.featureflip_initiate_login_uri == null hide_ios = true}resource "okta_app_group_assignments" "featureflip" { app_id = okta_app_oauth.featureflip.id group { id = data.okta_group.featureflip_users.id }}output "client_id" { value = okta_app_oauth.featureflip.client_id}output "client_secret" { value = okta_app_oauth.featureflip.client_secret sensitive = true}"> Run it twice. The first `terraform apply` creates the app with no tile. Read the credentials for step 4 with `terraform output client_id` and `terraform output -raw client_secret`, then save the connection in Featureflip. Copy the **Initiate login URI** it shows and apply again: ```bash terraform apply -var 'featureflip_initiate_login_uri=https://api.featureflip.io/api/management/v1/sso//start' ``` /start'"> The client secret is stored in your Terraform state in plain text, so keep that state somewhere encrypted. ## Offboarding Unassigning or deactivating someone in Okta stops them signing in again. Removing a user at your IdP ends their sessions within 24 hours. Also remove them from the organization in Featureflip to cut off their API tokens. ## Troubleshooting **“The provider identifies itself as ”…”. Use that exact value as the issuer.”** Copy the quoted issuer into **Issuer URL**. The usual cause is a trailing slash, or the org domain entered while the custom authorization server’s issuer is expected (or the reverse). **“Your identity provider rejected the test sign-in. Check the client ID and secret.”** Copy both again from the app’s **General** tab. Check that the secret you pasted is still active under **Client Secrets**. **Okta shows an error about the redirect URI.** The **Sign-in redirect URIs** entry must match the Featureflip value exactly, including `https` and the full path. **Okta says you aren’t assigned to the application.** Assign the user, or one of their groups, to the Featureflip app under **Assignments**. **“The test sign-in worked, but that account’s email isn’t on a verified domain.”** The Okta user’s primary email is on a domain you haven’t verified in Featureflip. Verify it, or test with an account on a verified domain. The [single sign-on overview](/docs/guides/single-sign-on/#troubleshooting) lists the remaining messages. --- ## Source: https://featureflip.io/docs/guides/targeting-rules/ # How to Set Up Targeting Rules Targeting rules let you control which users see which variation of a feature flag. Instead of toggling a flag on for everyone, you can roll out features to specific users based on attributes like email, plan, country, or any custom property. ## Where to find targeting - Open a flag from the **Flags** page at `/flags`. - On the flag detail page, select an **environment tab** (e.g., Development, Staging, Production). - The targeting configuration panel appears below the toggle. Targeting rules only apply when the flag is **on**. When the flag is off, all users receive the off variation. ## Creating a rule - Click **Add Rule** in the targeting panel. - **Choose an attribute** from the evaluation context (e.g., `email`, `plan`, `country`, `user_id`, or any custom attribute your application sends). - **Choose an operator:** - `equals` / `not equals` — Exact match. - `contains` / `not contains` — Substring match. - `starts with` / `ends with` — Prefix or suffix match. - `in list` / `not in list` — Match against multiple values. - **Enter the values** to match against. - **Select the variation** to serve when this rule matches. - Click **Save** to apply the rule. ## Rule evaluation order Rules are evaluated **top-to-bottom**. The first rule that matches a user’s context determines the variation they receive. No further rules are checked after a match. - **Drag rules** to reorder them in the targeting panel. - Place **more specific rules above general ones**. For example, put a rule targeting a single user above a rule targeting all Pro plan users. ## Fallthrough variation When a flag is on and **no targeting rules match** a user, the fallthrough variation is served. Configure the fallthrough variation in the environment configuration section below the rules list. Make sure the fallthrough variation is set to a sensible default for users who do not match any rule. ## Percentage rollouts Instead of serving a single variation, you can configure a rule or the fallthrough to use a **percentage rollout**. This distributes users across variations by percentage (e.g., 80% `false`, 20% `true`) for gradual feature launches. ## Testing evaluation Use the **Evaluate** feature on a flag to test what a specific user would receive: - Click the **Evaluate** button on the flag detail page. - Enter a JSON evaluation context (e.g., `{"user_id": "123", "plan": "pro", "country": "US"}`). - See which variation would be served and which rule matched. This is useful for verifying your targeting configuration before deploying changes. ## Example **Goal:** Give Pro plan users early access to a new feature. - Open the flag and select the Production environment. - Turn the flag **on**. - Click **Add Rule**. - Set attribute to `plan`, operator to `equals`, value to `pro`. - Select the `true` variation. - Set the fallthrough variation to `false`. - Save. Pro users now see the feature; everyone else does not. ## Next steps - [Targeting & Segments](/docs/concepts/targeting/) — understand evaluation context, rule ordering, and segments - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [How to Use Segments](/docs/guides/segments/) — create reusable user groups for targeting - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and configure flags before adding rules --- ## Source: https://featureflip.io/docs/guides/team-management/ # How to Manage Your Team Featureflip uses organizations to group projects, flags, and team members. This guide covers how to manage your team from the dashboard. ## Organizations Your account belongs to one or more organizations. Each organization has its own: - Projects and feature flags. - Environments and SDK keys. - Team members and roles. - Billing plan and usage limits. Switch between organizations using the organization selector in the navigation bar. ## Inviting members - Go to **Organization Settings** at `/orgs/:orgId/settings`. - Click **Invite Member**. - Enter the team member’s email address. - Click **Send Invite**. The invited person receives an email with a link to join your organization. They will need to create a Featureflip account if they do not already have one. ## Member roles Each member is assigned one of four roles: | Role | Permissions | | --- | --- | | Owner | Full access to everything including billing, organization settings, and member management. | | Admin | Manage flags, environments, segments, and members. Cannot access billing. | | Member | View and edit flags and targeting rules. Cannot manage environments or members. | | Viewer | Read-only access to flags and settings. | Every organization has at least one Owner. The person who creates the organization is automatically the Owner. Role changes and member removals are themselves recorded in Featureflip’s [audit log](/product/features/audit-log/) , so you always have a record of who granted or revoked access, and when. ## Managing members From the **Members** section in Organization Settings: - **Change a member’s role** — Click the role dropdown next to their name and select the new role. - **Remove a member** — Click **Remove** next to their name. They immediately lose access to the organization’s projects and flags. On Pro and above, removing someone who owns flags also asks who should take those flags over. Pick a teammate under **Reassign their flags to**, or leave it on **Leave unowned**. See [Flag Owners](/docs/guides/flag-owners/#when-someone-leaves-the-organization) for what happens to the flags either way. On the Business and Enterprise plans, people can also join through [single sign-on](/docs/guides/single-sign-on/) with your identity provider. Members who joined that way carry an **SSO** badge in the list. Removing them here also revokes their API tokens, which removing them at your identity provider alone doesn’t do. ## Pending invites View all outstanding invitations in the **Invites** section of Organization Settings: - See who has been invited and when the invite was sent. - **Revoke an invite** if it was sent to the wrong address or is no longer needed. Revoked invite links stop working immediately. ## Plan limits The number of team members you can have depends on your organization’s plan: | Plan | Member limit | | --- | --- | | Free | Up to 3 members | | Team | Up to 5 members | | Pro | Up to 10 members | | Business | Up to 25 members | | Enterprise | Unlimited | Pending invitations count toward the limit until they are accepted, revoked or expire. If you need to add more members than your plan allows, upgrade your plan from the **Billing** section in Organization Settings. New seats become available immediately after upgrading. ## Next steps - [Audit Log](/docs/guides/audit-log/) — see who on your team changed what, and when - [Flag Owners](/docs/guides/flag-owners/) — name who is responsible for each flag - [Plans and Limits](/docs/concepts/plans-and-limits/) — every plan’s limits in one table - [How to Create Feature Flags](/docs/guides/creating-flags/) — start creating flags for your team - [How to Manage Environments](/docs/guides/environments/) — set up environments for your workflow - [What Are Feature Flags?](/docs/concepts/feature-flags/) — learn the core concepts --- ## Source: https://featureflip.io/docs/guides/webhooks/ # Outbound Webhooks Outbound webhooks push flag and configuration changes to an endpoint you control, the moment they happen, instead of you polling the API on a timer. Toggle a flag, edit a targeting rule, rotate an SDK key. Featureflip sends a signed HTTP request describing what changed, and you can trigger a deploy from it, post it to a chat channel, refresh a cache, or feed your own audit pipeline. Every delivery is signed, retried on failure, and logged. You can see exactly what was sent, and when. ## Creating a subscription ### In the dashboard - Open **Organization Settings**. - In the **Webhooks** card, click **Add webhook**. - Enter a **name** and the **URL** to receive events. The URL must be a public `https://` address. Featureflip refuses to deliver to a private network or a loopback address. - Choose which **event types** to receive, and optionally scope the subscription to specific **projects** and **environments**. The environment picker follows your project selection, so pick a project first to see its environments. - Save. The signing secret is shown once, right after creation, so copy it before closing the dialog. Only organization admins can view or manage webhooks. How many subscriptions an organization can hold depends on its plan: one on Free, three on Team and Pro, and as many as you need on Business and Enterprise. ### Via the API ```plaintext POST /api/v1/orgs/{org}/webhooks ``` ```json { "name": "Deploy pipeline", "url": "https://ci.example.com/hooks/featureflip", "provider": "GenericHttp", "eventTypes": ["flag.toggled", "flag.environment_config.updated"], "projectIds": [], "environmentIds": [] } ``` Response `201 Created`: ```json { "id": "0199f3c1-1a2b-7c3d-9e4f-5a6b7c8d9e0f", "secret": "whsec_9k3mF2pQx8vL1nR7tY6wZ4aB0cD5eG...", "warning": "Store this secret securely — it will not be shown again." } ``` As in the dashboard, the secret comes back only in this response, and there’s no way to read it later. If you lose it, rotate it instead (see Rotating signing secrets ). `provider` must be `"GenericHttp"`. This create call, like other create calls on the public API, accepts an `Idempotency-Key` header. See [Conventions](/docs/management-api/conventions/) for how it works. ### With Terraform The [Terraform provider](/docs/integrations/terraform/) (0.3.0 and later) manages subscriptions as a `featureflip_webhook` resource, so a new receiver goes through the same pull request as the flags it listens to: ```hcl resource "featureflip_webhook" "deploys" { name = "Deploy pipeline" url = "https://ci.example.com/hooks/featureflip" event_types = ["flag.toggled", "flag.environment_config.updated"] } ``` The signing secret lands in the resource’s sensitive `secret` attribute. Like the API response, it’s the only copy. From then on it lives in Terraform state. ### From an AI agent The [MCP server](/docs/integrations/mcp/) (`@featureflip/mcp` 0.1.8 and later) has four webhook tools, so Claude Code, Cursor or any other MCP client can do this work for you: | Tool | What it does | | --- | --- | | list_webhooks | Lists your subscriptions and the event types they can filter on | | list_webhook_deliveries | Shows one subscription’s delivery attempts and how each one went | | deliver_webhook | Sends a test event, or redelivers a past delivery | | manage_webhook | Creates, updates or deletes a subscription, or rotates its signing secret | All four need an Admin token, even the two that only read. When `manage_webhook` creates a subscription or rotates a secret, the tool result carries the new signing secret, and that’s the only time you’ll see it. ## Choosing event types and scope An empty `eventTypes` list (or none selected in the dashboard) means every event type. List specific ones to narrow it. The full catalog is available at any time from: ```plaintext GET /api/v1/orgs/{org}/webhooks/event-types ``` Event types are grouped by the entity they describe: flags (`flag.created`, `flag.updated`, `flag.toggled`, `flag.archived`, `flag.restored`, `flag.deleted`, `flag.environment_config.updated`), targeting rules, variations, segments, environments, projects, SDK keys, API tokens, and the organization itself (each of the latter eight as `.created` / `.updated` / `.deleted`, or `organization.updated` alone). Project and environment filters work the same way: leave them empty for everything. There’s one difference worth knowing, though. A flag’s own lifecycle events, like `flag.created`, `flag.updated`, `flag.archived`, and `flag.deleted`, along with the equivalent events for variations, segments, and projects, aren’t scoped to a single environment, since the entity itself spans every environment in its project. An environment filter can’t narrow those. Only a project filter can. What the environment filter does narrow is the events that genuinely happen in one environment: a flag toggling, or its per-environment configuration changing. Combine a project filter with an environment filter if you want both narrowed together. Deleting a project or environment removes it from every subscription’s filters. If that leaves a filter empty, the subscription is disabled too, because an empty filter means everything and would otherwise start sending the endpoint events it never asked for. Either way, a subscription that was watching the deleted project or environment gets the `project.deleted` or `environment.deleted` event, including one the same deletion disabled. If the deletion disabled it, that event is the last one it receives. Set new filters before re-enabling it. ## Payload format Every delivery’s body is a single JSON object describing one change. Here’s a `flag.toggled` event, sent when a flag is turned on or off in an environment: ```json { "type": "flag.toggled", "occurredAt": "2026-09-23T14:32:07.412Z", "organizationId": "0199f2aa-1111-7222-8333-444455556666", "projectId": "0199f2aa-2222-7222-8333-444455556666", "environmentId": "0199f2aa-3333-7222-8333-444455556666", "actor": { "userId": "0199f2aa-4444-7222-8333-444455556666", "email": "jane@example.com", "name": "Jane Doe" }, "entity": { "type": "FeatureFlag", "id": "0199f2aa-5555-7222-8333-444455556666" }, "child": { "type": "FlagEnvironmentConfiguration", "id": "0199f2aa-6666-7222-8333-444455556666" }, "changes": [ { "property": "IsEnabled", "from": "false", "to": "true" } ], "description": "Toggled \"new-checkout\" on in Production" } ``` A few fields worth knowing: - **entity** always identifies the flag, segment, project, or other top-level object the change belongs to: the thing you’d look up through the API. **child** is present only when the change happened on something beneath that entity, such as a targeting rule, a variation, or, as here, a flag’s per-environment configuration. It names the specific child object that changed. - **changes** lists each property that moved, with its previous and new value as strings. Property names match the API’s own field casing, so it’s `IsEnabled`, not `isEnabled`. They aren’t relabeled for the payload. - **occurredAt** is when the change was made, not when the request was sent. Use it to order events. See Delivery order below. - Creating a flag with its initial variations produces one `flag.created` event with a `children` array listing what was created alongside it, rather than a separate event per variation. ## Verifying the signature Every delivery carries three headers: ```plaintext webhook-id: 0199f3c2-7a1b-7c2d-9e3f-4a5b6c7d8e9f webhook-timestamp: 1758640327 webhook-signature: v1,4mF2pQx8vL1nR7tY6wZ4aB0cD5eG9k3mF2pQx8vL1n= ``` `webhook-id` is the event’s own id. It’s stable across every retry of the same event, which makes it the right key for deduplication (see Delivery order ). To verify, HMAC-SHA256 the string `{webhook-id}.{webhook-timestamp}.{raw request body}` with your signing secret, base64-encode the result, and compare it against the value after `v1,` in the header. Two details matter: - The secret is `whsec_`-prefixed base64. Strip the prefix, base64-decode it, and that’s your actual HMAC key. Don’t hash the prefixed string itself. - Sign the **raw** request body, exactly as received. Parsing it as JSON and re-serializing changes key order and whitespace, and the signature won’t match. If you’ve rotated your secret recently, `webhook-signature` can carry more than one `v1,...` entry, space-separated, one per active secret. Accept the delivery if any of them matches. A signature that matches tells you Featureflip sent the delivery. It doesn’t date it. Anyone who captures one signed request can resend it an hour or a month later, and it will verify every time. So read `webhook-timestamp` as well, and reject any delivery stamped more than five minutes before or after your server’s clock, the tolerance [Standard Webhooks](https://www.standardwebhooks.com/) recommends. Featureflip re-signs every attempt with the time it goes out, including retries and redeliveries you trigger by hand. A genuine delivery always lands inside the window. If valid deliveries start failing the check, suspect clock drift and keep the server synced with NTP. **Node.js:** ```js import crypto from 'node:crypto'; const TOLERANCE_SECONDS = 5 * 60; function verifyWebhook(secret, webhookId, webhookTimestamp, rawBody, signatureHeader) { // Refuse anything stamped more than five minutes from now, in either direction const timestamp = Number(webhookTimestamp); const now = Math.floor(Date.now() / 1000); if (!Number.isInteger(timestamp) || Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false; const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`; const expected = crypto.createHmac('sha256', key).update(signedContent).digest(); return signatureHeader.split(' ').some((entry) => { const [version, value] = entry.split(','); if (version !== 'v1' || !value) return false; const candidate = Buffer.from(value, 'base64'); return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected); }); } ``` TOLERANCE_SECONDS) return false; const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`; const expected = crypto.createHmac('sha256', key).update(signedContent).digest(); return signatureHeader.split(' ').some((entry) => { const [version, value] = entry.split(','); if (version !== 'v1' || !value) return false; const candidate = Buffer.from(value, 'base64'); return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected); });}"> **Python:** ```python import base64 import binascii import hashlib import hmac import time TOLERANCE_SECONDS = 5 * 60 def verify_webhook(secret, webhook_id, webhook_timestamp, raw_body, signature_header): # Refuse anything stamped more than five minutes from now, in either direction try: timestamp = int(webhook_timestamp) except (TypeError, ValueError): return False if abs(time.time() - timestamp) > TOLERANCE_SECONDS: return False key = base64.b64decode(secret.removeprefix("whsec_")) # raw_body is the request body as bytes, exactly as received signed_content = f"{webhook_id}.{webhook_timestamp}.".encode() + raw_body expected = hmac.new(key, signed_content, hashlib.sha256).digest() for entry in signature_header.split(" "): version, _, value = entry.partition(",") if version != "v1" or not value: continue try: candidate = base64.b64decode(value) except binascii.Error: continue # a malformed entry is a non-match, not an error if hmac.compare_digest(candidate, expected): return True return False ``` TOLERANCE_SECONDS: return False key = base64.b64decode(secret.removeprefix("whsec_")) # raw_body is the request body as bytes, exactly as received signed_content = f"{webhook_id}.{webhook_timestamp}.".encode() + raw_body expected = hmac.new(key, signed_content, hashlib.sha256).digest() for entry in signature_header.split(" "): version, _, value = entry.partition(",") if version != "v1" or not value: continue try: candidate = base64.b64decode(value) except binascii.Error: continue # a malformed entry is a non-match, not an error if hmac.compare_digest(candidate, expected): return True return False"> Read the raw body before your framework parses it as JSON: bytes for the Python example, a string or Buffer for Node. Most frameworks expose this as a raw-body or bytes option on the route. ## Retries and timeouts Featureflip attempts each delivery up to **6 times**: the initial attempt, then retries after **10 seconds, 1 minute, 5 minutes, 30 minutes, and 2 hours** if the previous attempt didn’t succeed. If every attempt fails, the last one lands a little over two and a half hours after the first. Each attempt has a **10-second timeout**. A delivery counts as successful the moment your endpoint responds with any `2xx` status code, and the response body itself is never read. Anything else counts as a failed attempt and schedules the next retry: a non-2xx status, a timeout, a connection failure. If every attempt fails, the delivery is marked **failed** (shown as `DeadLettered` in the API). It stays in the delivery log, and you can send it again by hand. See Viewing and redelivering below. A subscription that fails **20 delivery rounds in a row** disables itself automatically. A round is one batch of attempts to your endpoint, not one delivery, so many events failing together against the same unresponsive endpoint count once toward that total, not once each. While a subscription is disabled, deliveries that come due are marked failed rather than sent. Re-enable it from the dashboard, or `PUT` the subscription with `isEnabled: true`, then redeliver whatever you still need. ## Delivery order and idempotency Deliveries go out concurrently, so arrival order isn’t guaranteed. A retried delivery can land well after events that happened later. Treat each delivery as a signal to re-read current state through the API rather than assuming it arrived in order. If you do apply `changes` directly, order by the payload’s `occurredAt`, not by arrival time or by the `webhook-timestamp` header. That header is re-stamped on every retry attempt, so it doesn’t reflect when the change actually happened. Track the newest `occurredAt` you’ve applied per changed property, keyed by `child.id` when present, otherwise `entity.id`, plus the property name, and skip anything older. This also protects against duplicates: delivery is at-least-once, so the same event can arrive more than once, and `webhook-id`, stable across retries, is the field to dedupe on. ## Viewing and redelivering Each subscription’s page shows its recent deliveries: status, attempt count, the last response code and error, and when it was created and completed. The same data is available from the API: ```plaintext GET /api/v1/orgs/{org}/webhooks/{id}/deliveries ``` Status is one of `Pending` (queued or retrying), `Succeeded`, or `DeadLettered`. To resend a specific delivery, say after fixing whatever made your endpoint fail, redeliver it: ```plaintext POST /api/v1/orgs/{org}/webhooks/{id}/deliveries/{deliveryId}/redeliver ``` This resets the delivery’s retry schedule from scratch. It’s refused with a `400` if the delivery is still pending, if the subscription is disabled, or if its filters no longer cover the delivery’s event. The deletion event described in Choosing event types and scope is the exception. You can redeliver that one even if the subscription is now disabled or no longer covers it. To check a new subscription end to end without waiting for a real change, send a synthetic test event: ```plaintext POST /api/v1/orgs/{org}/webhooks/{id}/test ``` This queues one `flag.toggled` event to that subscription alone. It’s asynchronous, so check the delivery log for the outcome, and it’s refused with a `400` on a disabled subscription. ## Rotating signing secrets Rotating adds a new active secret without removing the old one: ```plaintext POST /api/v1/orgs/{org}/webhooks/{id}/secrets ``` The response carries the new secret’s value, shown once, the same as at creation. From that point on, every delivery is signed with **every** active secret, and the `webhook-signature` header carries one entry per secret. That means your endpoint can start verifying against the new value before you’ve fully switched over: accept the delivery if any signature matches a secret you recognize. Once your endpoint verifies with the new secret, retire the old one: ```plaintext DELETE /api/v1/orgs/{org}/webhooks/{id}/secrets/{secretId} ``` Featureflip refuses to retire a subscription’s last active secret. Rotate first, then retire. Never the other way around. In Terraform, a `featureflip_webhook_secret` resource adds a secret and destroying it retires one. The [Terraform provider page](/docs/integrations/terraform/#webhooks) covers how to retire the secret a webhook was created with. ## Next steps - [Feature Flag Audit Log](/docs/guides/audit-log/) — the same changes, browsable in the dashboard - [Management API Reference](/docs/management-api/reference/) — every webhook endpoint, with full request and response shapes - [Authentication](/docs/management-api/authentication/) — create a token to manage webhooks from the API - [MCP Server](/docs/integrations/mcp/) — manage webhooks and read the delivery log from an AI agent - [Conventions](/docs/management-api/conventions/) — pagination, idempotency, and the error format shared by every endpoint - [Creating Feature Flags](/docs/guides/creating-flags/) — the flags whose changes these webhooks report --- ## Source: https://featureflip.io/docs/integrations/flag-cleanup-action/ # Flag Cleanup Action Detecting a dead flag is the easy half. The Featureflip flag cleanup Action does the other half: it reads removal candidates from the public API, deletes the flag’s code path in your repository, and opens one pull request per flag for you to review. Merge it, ship it, and the flag is archived, so the code and the dashboard stop disagreeing. It runs inside your CI, in your repository, holding your token. No LLM is involved: the rewrites are AST transforms, so the same input always produces the same diff. ## Installation Add a workflow to the repository you want cleaned up. Both permissions below are required. ```yaml name: Featureflip flag cleanup on: schedule: - cron: '0 9 * * 1' # Mondays, 09:00 UTC workflow_dispatch: {} permissions: contents: write # push the removal branch pull-requests: write # open the pull request jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: canopy-labs/featureflip-flag-cleanup-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: api-token: ${{ secrets.FEATUREFLIP_API_TOKEN }} org: my-org project: my-project dry-run: true # first run: read the diffs before anything is opened ``` `FEATUREFLIP_API_TOKEN` is your own token, stored as a repository or organization secret. A read-only Viewer token scoped to the one project is enough for this workflow, and is what these instructions are verified against. **Start with dry-run: true.** It computes and prints every diff without contacting GitHub at all. Every refusal a real run can produce is reachable in a dry run and reported identically, so the preview cannot approve something the next real run declines. The default is `false`, which opens real pull requests on the first run. ## The five modes `mode` decides what one invocation does. Each mode is a separate workflow on its own trigger, never a step inside a shared job, because the triggers differ and so do the tokens. | mode | Trigger | What it does | | --- | --- | --- | | remove (the default) | schedule | Opens one removal pull request per dead flag | | archive-on-merge | pull_request, types: [closed] | Archives the flag whose removal just merged, or defers it | | archive-sweep | schedule | Retries the archives that deferred | | archive-on-deploy | deployment_status | Archives the outstanding flags the deployed commit contains | | pr-command | issue_comment, types: [created] | Rebuilds a removal when you comment on its pull request | Most repositories end up running three of those: the scheduled `remove` workflow above, `archive-on-merge`, and `archive-sweep`. Those last two work as a pair, for a reason the next two sections spend their time on. ## Archive the flag once the removal is deployed Merging the cleanup PR removes the code. The flag itself stays live until something archives it. `mode: archive-on-merge` starts closing that loop, as a second workflow on a different trigger: ```yaml name: Featureflip archive on merge on: pull_request: types: [closed] jobs: archive: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - uses: canopy-labs/featureflip-flag-cleanup-action@v1 with: mode: archive-on-merge api-token: ${{ secrets.FEATUREFLIP_ARCHIVE_TOKEN }} org: my-org project: my-project ``` **The loop closes on the deploy, not on the merge.** Those are the same minute in some pipelines and days apart in others, and which one you have decides how this mode behaves for you. Archiving is not a soft state. The flag leaves the evaluation payload within seconds, every connected SDK drops it, and each read falls back to the default value written beside it at the call site. If a build that still reads the flag is running in production at that moment, that fallback is what production serves from then on. Which way the feature moves is not a coin flip. This Action only proposes flags whose telemetry says they have served one variation and nothing else for months. For a flag that is fully rolled out, that variation is the on one, while the default beside the read is the value its author wrote for the case where the flag cannot be reached at all, which is almost always the off one. So archiving a rolled-out flag ahead of its deploy does not disturb the feature at random. It turns the feature **off**, for everyone, until the deploy lands. **Featureflip refuses the archive while it can still see the flag being evaluated**, which is what stops that being a silent outcome. The run reports `[deferred]` and exits `0`. Nothing failed and nothing is yours to fix, because the refusal lifts by itself once the deploy lands and the old traffic drains. **So wire up archive-sweep as well.** A deferred archive that nothing retries is a leak: the flag stays live forever and every run keeps exiting `0`, which is exactly the half-closed loop this mode exists to end. Wherever your merges and your deploys are separate events, `[deferred]` is the ordinary result of a removal merge and the sweep in the next section is what actually archives the flag. Three more things are worth knowing about it. **It needs a different token, and that is why it is a separate workflow.** Archiving requires the Member role, while reading removal candidates needs only read access. Give this workflow its own secret. If a read token leaks it discloses flag names. A Member token can change flag state. **It needs no checkout and no permissions block.** The run reads the pull request that triggered it and makes one API call. It touches no source and never calls the GitHub API. **It identifies the flag from the branch name**, which the Action encodes reversibly when it opens the pull request. Nothing is read from the title or body, so editing those is safe, and so is squashing the merge. The workflow above fires on every closed pull request. That is expected: anything this Action did not open is skipped with a one-line `[not-a-removal-branch]` and exits `0`. A pull request you close **without** merging is likewise `[not-merged]` and leaves the flag alone, because declining a removal should never archive anything. Archiving is idempotent, so a redelivered event or a re-run is harmless. It can also fail for a reason no retry clears, and those turn the run red instead of deferring. The message says which case you are in: a flag that another live flag lists as a prerequisite cannot be archived until that prerequisite is gone, and neither can one with a scheduled change still pointing at it. Both need a person. ## Drain the archives that deferred `mode: archive-sweep` is what comes back for the flags above. On a schedule, it finds every flag in the project whose removal pull request merged but which is still live, and tries to archive each one again: ```yaml name: Featureflip archive sweep on: schedule: - cron: '0 6 * * *' workflow_dispatch: {} permissions: pull-requests: read # did this flag's removal branch ever merge? jobs: sweep: runs-on: ubuntu-latest steps: - uses: canopy-labs/featureflip-flag-cleanup-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: mode: archive-sweep api-token: ${{ secrets.FEATUREFLIP_ARCHIVE_TOKEN }} org: my-org project: my-project ``` **Run this, not just archive-on-merge.** The two are one mechanism split across two triggers, and the merge half is the half that usually cannot finish. Skipping the sweep is how a repository ends up with a tidy source tree and a dashboard full of flags nobody removed. **It needs GITHUB_TOKEN, and archive-on-merge does not.** That is the one real cost of the mode. Not firing on the merge event means it has no pull request handed to it, so it asks GitHub once per live flag whether that flag’s removal branch ever produced a merged pull request. Read access to pull requests is all it wants, and it writes nothing to your repository. **Evidence of a merge is what authorizes each archive.** A removal pull request you closed without merging is a removal you declined, and it reads to this mode exactly like a flag that was never proposed. Nothing is archived that this Action did not open a pull request for. **It needs the same Member-role token**, for the same reason, and it keeps the same quiet and loud split. `archived` and `deferred` are both fine and exit `0`. A flag the API refuses for any other reason is named on its own line and exits `1` without stopping the rest of the backlog draining. **There is no state to manage.** Nothing is written when an archive defers, because a flag still being live is the record, so running this more often, less often or twice at once is harmless and there is nothing to reset if you turn it off. `dry-run: true` prints the flags it would attempt and sends nothing. A run with nothing to do prints a line saying so. One case is genuinely not covered, and no amount of retrying fixes it: **you ship to clients you cannot update.** Mobile apps, desktop apps and embedded devices keep evaluating the flag for as long as people keep running the old version, which can be months, so the refusal never clears on its own and the sweep defers indefinitely. That is the correct behavior, since the flag really is still being read, but it means archiving those is a decision rather than a schedule. Archive them by hand once the old versions are gone. ## Archive when the deploy actually lands `archive-on-merge` fires on the merge, which is the wrong event: it proposes an archive against a commit nobody is running yet. If your pipeline creates **GitHub Deployments**, `mode: archive-on-deploy` fires on the deploy instead, and archives only the outstanding flags whose removal that deployment actually contains: ```yaml name: Featureflip archive on deploy on: deployment_status permissions: contents: read # is the removal's merge commit in the deployed one? pull-requests: read # which merged removals exist? jobs: archive: runs-on: ubuntu-latest steps: - uses: canopy-labs/featureflip-flag-cleanup-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: mode: archive-on-deploy api-token: ${{ secrets.FEATUREFLIP_ARCHIVE_TOKEN }} org: my-org project: my-project deployment-environment: production ``` **It does not replace the sweep, and it does not avoid the deferral.** Featureflip refuses an archive while it can still see the flag being evaluated, and a deployment finishes seconds after the build it replaced was serving that flag, so for any flag with live traffic this mode’s ordinary outcome is `[deferred]` too. Run `archive-sweep` on a schedule alongside it. Without one, a repository that deploys weekly defers every removal and never drains. What this mode adds is the half the refusal cannot do. The refusal reads traffic your environments have already reported, so it can only rule on environments it has seen traffic from, and it lets through a flag nothing has evaluated. That is right when the flag really is dead and wrong when the code reading it is deployed but rarely reached. This mode asks git instead, and git cannot be vague about a commit: either your deployment contains the removal or it does not. The two checks fail in opposite directions, which is the whole reason to run both. Then read the two limits, because they decide whether this mode is worth wiring up at all. **It only helps if something creates GitHub Deployments.** Plenty of pipelines do, including Vercel, Netlify, `actions/deploy-pages`, and any workflow that names a GitHub `environment:`. Plenty do not, and there is no event for this mode to hang on in those. Nothing is lost by skipping it: `archive-on-merge` plus `archive-sweep` behave exactly as they did. **It does nothing for clients you cannot update.** Mobile, desktop and embedded builds keep evaluating the flag long after your servers stop, and no git event corresponds to the last old version stopping. Deploying is simply not the completion event there, which is why those flags stay covered by Featureflip’s own refusal and the `[deferred]` outcome described above. **It asks whether the deployed commit contains the removal, and never assumes.** A flag whose removal is not in that commit yet is reported as `[pending-deploy]` and left alone for the deploy that ships it, and nothing is written to record that, so nothing has to be cleaned up. A flag GitHub will not answer for at all is named and turns the run red. *I could not tell* is a different answer from *not yet*, and it will not clear by itself. The rest of the backlog is archived either way. **A deployment to another environment archives nothing**, and says so out loud, because a workflow that quietly never fires is indistinguishable from a repository with nothing to clean up. `deployment-environment` is what names the one you archive on, and it is matched case-insensitively. Any status that is not `success` is likewise reported and archives nothing, since a deployment that failed left the old build running. ## Re-run a removal from a comment The scheduled sweep proposes each flag once. After a pull request exists for it, open or closed, the sweep leaves that flag alone, which is what stops a removal you declined from coming back every Monday. A removal can still go stale in place. The base branch moves on, or you fix an `accessors` entry and want the diff it should have produced in the first place. `mode: pr-command` rebuilds it when you ask on the pull request. It runs as a third workflow, on comments: ```yaml name: Featureflip cleanup commands on: issue_comment: types: [created] permissions: contents: write # force-push the rebuilt branch pull-requests: write # update the pull request and reply jobs: pr-command: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # your default branch, where the rebuild starts - uses: canopy-labs/featureflip-flag-cleanup-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: mode: pr-command api-token: ${{ secrets.FEATUREFLIP_API_TOKEN }} org: my-org project: my-project # If the scheduled workflow sets directories, languages, # accessors or ignore, copy the same values here. ``` Then comment on the removal pull request, with the command on a line of its own: - **@featureflip-cleanup rerun** rebuilds the removal from the pull request’s own base branch and force-pushes the result over its branch. It refuses if anyone other than the Action has committed there, so a fix-up you pushed by hand never disappears without you asking. - **@featureflip-cleanup recreate** does the same rebuild and pushes over those commits, discarding them. Neither command takes a flag key. The flag comes from the pull request’s branch name, the same encoding archive-on-merge reads, so a comment can’t point a re-run at a different flag. The push also carries a lease on the branch’s current commit. Push to the branch while a re-run is working and the Action’s push is rejected, leaving yours intact. **Only people with write access can run a command.** Anyone else’s comment gets no reply. `issue_comment` fires for everyone who can comment, and answering all of them would hand a megaphone to whoever types the mention. **Copy the transform inputs from the scheduled workflow.** `directories`, `languages`, `accessors` and `ignore` feed the same rewrite the sweep runs, so a mismatch rebuilds a different diff under the same title. `org`, `project` and `api-url` have to name the same project too. The sweep’s read-only token is enough, because this mode never changes a flag. The other inputs do nothing here. `staleness` can’t change the diff, since the command looks the flag up in both tiers. `flags`, `max-prs` and `pr-labels` only shape a sweep that opens new pull requests, and the rebuild always starts from the pull request’s own base, whatever `base-branch` says. **A key in ignore is refused.** Ignoring a flag is how you silence it, and a comment shouldn’t bring it back. **dry-run: true previews without pushing.** The rebuild runs in full and the reply lists what the push would have done, down to how many commits it would have replaced. This dry run does talk to GitHub, unlike the sweep’s. It checks the commenter’s access and reads the pull request before replying, and skips only the push and the pull request update. **A closed pull request is reopened by the rebuild.** A merged one is refused outright, since its code is already on the base branch and there is nothing left to remove. In this mode an exit code of `0` means the comment was answered. That covers comments the Action ignores, every successful rebuild, a dry run, and an empty rebuild, which tells you nothing in the configured directories reads the flag any more and the pull request can be closed. `1` is a refusal or a failed rebuild. Commits from someone else without `recreate`, a merged pull request, a flag that stopped being a removal candidate and a key in `ignore` all land here, and each gets a reply saying which. `2` means the run never started, and nothing was pushed. The usual causes are a workflow listening to something other than `issue_comment`, or a checkout with uncommitted changes to tracked files. ## When to reach for this, and when to reach for the MCP server Featureflip has two ways to get a finished flag out of your code, and they are built to cover different halves of the problem. **This Action is the unattended half.** It runs on a schedule, uses no LLM, and produces the same diff for the same input every time. That makes it right for the mechanical bulk: a flag read at a literal key, in a shape the rewrite rules recognize, in any of thirteen languages. It opens a pull request and archives the flag once the removal is deployed. **The MCP server is the interactive half.** Your agent calls `find_stale_flags`, you confirm which ones are genuinely finished, and the agent removes the conditional in your editor. The server supplies the staleness data and performs the flag mutation. It never edits files on its own. The handoff between them is the declined-shape list below. A flag read behind your own wrapper, or keyed by a variable instead of a literal, is exactly what this Action refuses and exactly what an agent reading the surrounding code handles well. So a declined flag is not a dead end, it is a pointer to the other path. Most teams end up running the Action weekly and reaching for the agent on whatever it names. ## Supported languages Removal runs across TypeScript, TSX, JavaScript, Java, Go, Python, Kotlin, C#, PHP, Ruby, ERB, Dart and Swift. That covers every language a Featureflip SDK is written for, plus the Rails view templates that read those flags: a flag branched on inside an `.erb` view is removed in the same pull request as the controller that used to set it up. Your Android, iOS and Flutter code is handled the same way your backend is, by the same run and the same pull request. Leave `languages` unset to run all of them, or name a subset to narrow a run. ## What it declines to touch The list of shapes it refuses is the specification, not a gap. Each one exists because folding it would change behavior: - **A flag read behind your own wrapper.** Most codebases wrap the SDK. Name the wrapper in `accessors` and it is matched exactly as an SDK call would be. Without that the wrapper is invisible and the flag reports no changes. - **A key that is not a literal at the call site.** A key built from a variable or a template cannot be matched by name. - **A binding another part of the file could still reach**, which a file-scoped check determines before any rewrite runs. - **Any file whose rewrite does not survive a re-parse.** Every changed file is parsed again and compared against its input. A file that fails is abandoned along with the whole flag, never committed and hoped over. A declined flag is reported by name with the reason, and it turns the run red rather than passing quietly. Add it to `ignore` to silence it deliberately. ## Before your first real run - **max-prs defaults to 10.** A project with two hundred dead flags would otherwise get two hundred branches on the first morning. Nothing is lost: the next run continues where this one stopped, because flags already proposed are skipped. - **staleness defaults to dead.** Dead flags open ready-for-review pull requests, and `stale` opens drafts. - **flags limits a run to the keys you list.** Paired with `dry-run: true`, it prints one flag’s diff without contacting GitHub at all, a quick way to judge a single removal before letting a run near the rest. It never re-proposes a flag that already has a pull request (comment `rerun` on that one instead), and a key in both `flags` and `ignore` stays skipped. - **Exit codes.** `0` means every candidate the run attempted succeeded, including “nothing to do”. `1` means at least one flag could not be proposed. `2` means the run could not start at all, and nothing was modified. - **Run it from a checkout of your base branch.** Removal branches are cut from the checkout, so a `pull_request` merge commit would drag its commits into every pull request. ## Reference The full input reference, the per-language read shapes it recognizes, and the safety gates in detail live in the Action’s own repository: [canopy-labs/featureflip-flag-cleanup-action](https://github.com/canopy-labs/featureflip-flag-cleanup-action) . Related: [stale flag detection](/docs/concepts/stale-flags/) explains how flags are classified Active, Stale or Dead in the first place, and [what a bot must refuse](/blog/automated-feature-flag-removal/) covers why the declines above are the interesting part. --- ## Source: https://featureflip.io/docs/integrations/mcp/ # MCP Server The Featureflip MCP server (`@featureflip/mcp`) lets AI coding assistants and autonomous agents manage feature flags through the [Management API](/docs/management-api/overview/) : create and toggle flags, adjust targeting and rollouts, and find stale flags ready for cleanup — without leaving the editor. It runs as a local process over stdio via `npx`, so there’s nothing to deploy: your agent starts it, talks to it over the Model Context Protocol, and it calls the Management API on your behalf using whatever token you give it. It works on every plan, including Free. The Management API it calls is available on all plans, so you can create an account, generate a token, and let an agent manage real flags without a paid subscription. ## Installation You’ll need an API token first — see Authentication below. Once you have one, add the server to your MCP client. ### Claude Code ```bash claude mcp add featureflip -e FEATUREFLIP_TOKEN=ffp_your_token -- npx -y @featureflip/mcp ``` ### Cursor / Cline / generic JSON config Most other MCP clients (Cursor, Cline, and anything that reads a standard MCP config file) use a JSON block like this: ```json { "mcpServers": { "featureflip": { "command": "npx", "args": ["-y", "@featureflip/mcp"], "env": { "FEATUREFLIP_TOKEN": "ffp_your_token" } } } } ``` Both forms launch the same package: `npx -y @featureflip/mcp`. The `-y` flag skips npm’s install confirmation prompt, which matters because most MCP clients invoke the command non-interactively. ## Authentication The MCP server authenticates the same way the Management API does — with a bearer token passed via the `FEATUREFLIP_TOKEN` environment variable. There are two token types: - **Personal token** (`ffp_...`) — create one under **Settings → API Tokens** in the dashboard. It acts as you, carrying your role in every organization you belong to. This is the right choice for interactive editor use (Claude Code, Cursor) where you want the agent’s changes attributed to you. - **Service token** (`ffs_...`) — create one under **Organization Settings → Service Tokens**. It’s a machine identity scoped to a single organization, and the right choice for CI or unattended agents. See [Authentication](/docs/management-api/authentication/) for the full walkthrough of token types, roles, and error handling — the MCP server inherits all of it. A token below the required role for a write returns the same `403 forbidden` you’d see from a raw API call. ## Configuration | Env var | Required | Default | Purpose | | --- | --- | --- | --- | | FEATUREFLIP_TOKEN | yes | — | ffp_/ffs_ API token | | FEATUREFLIP_API_URL | no | https://api.featureflip.io | API base URL | | FEATUREFLIP_ORG | no | auto | Org slug (needed only for multi-org personal tokens) | Most setups only need `FEATUREFLIP_TOKEN`. Set `FEATUREFLIP_API_URL` if you’re pointing at a self-hosted or non-default deployment. Set `FEATUREFLIP_ORG` if your personal token belongs to more than one organization — without it, the server exits at startup with an error listing the org slugs it can see (it can’t prompt you interactively) and you re-run with `FEATUREFLIP_ORG` set to one of them. ## Tool reference The server exposes 25 tools. Read-only tools are always safe to call; destructive tools change or remove existing state and should be used deliberately (most agent clients will ask for confirmation before running them). Non-destructive writes create new state without touching anything existing. ### Read-only | Tool | What it does | | --- | --- | | list_projects | List projects in an organization | | list_environments | List environments in a project | | list_segments | List reusable user segments in a project | | get_segment | Get a single segment’s rules | | list_flags | List feature flags in a project | | get_flag | Get a single flag’s configuration | | flag_status | Cross-environment view of a flag’s enabled state, strategy, default variation, and prerequisites per environment | | get_targeting | Get a flag’s current targeting rules in one environment — read before you update_targeting | | find_stale_flags | Find flags that look like cleanup candidates | | list_webhooks | List webhook subscriptions and the event types they can filter on | | list_webhook_deliveries | List one subscription’s delivery attempts and their outcomes | ### Non-destructive writes | Tool | What it does | | --- | --- | | create_flag | Create a new feature flag (starts disabled in every environment) | | update_flag | Update a flag’s name, description, or metadata | | restore_flag | Restore an archived flag | | set_flag_expiry | Set or clear the date a flag should be removed by (evaluation doesn’t change) | | set_flag_owner | Set or clear the person responsible for a flag (evaluation doesn’t change) | | wrap_feature | Create a flag and return an SDK snippet to wrap your code with it | | deliver_webhook | Send a test event to a webhook, or redeliver a past delivery | ### Destructive | Tool | What it does | | --- | --- | | delete_flag | Permanently delete a flag | | archive_flag | Archive a flag | | toggle_flag | Enable or disable a flag in an environment | | update_flag_environment_config | Change a flag’s per-environment configuration | | update_targeting | Replace a flag’s targeting rules, conditions, and rollout percentage wholesale — read the current rules first with get_targeting | | manage_variation | Add, update, or remove a flag’s variations | | manage_webhook | Create, update, or delete a webhook subscription, or rotate its signing secret | The four webhook tools need an Admin token, reads included. When you create a subscription or rotate its secret, the response shows the signing secret. It is never shown again. ## Example agent workflows ### Wrap new code in a flag Ask the agent to gate a new feature behind a flag. It calls `wrap_feature`, which creates the flag (disabled everywhere, as all new flags are) and returns a code snippet using the SDK for your project’s language — the agent applies that snippet directly to your working tree. Once you’re ready to test it, ask the agent to enable it in dev: it calls `toggle_flag` for the `dev` environment. The flag stays off in every other environment until you say otherwise. ### Ramp a rollout To gradually roll out a flag that’s already live, ask the agent to ramp it: it calls `update_targeting` with `rolloutPercentage` set to `10`, then later `50`, then `100` as you gain confidence — each call replaces the previous percentage rather than stacking. Once the flag has been at 100% for a while, it’s a candidate for cleanup — `find_stale_flags` will start surfacing it. ### Clean up shipped flags Periodically ask the agent to find cleanup candidates. It calls `find_stale_flags`, which returns flags that have been fully rolled out or unchanged for a long stretch. `find_stale_flags` checks whether every environment is enabled, not whether a per-rule percentage ramp has actually reached 100% — confirm the rollout is complete before removing. For each one you confirm, the agent calls `delete_flag` and removes the corresponding conditional from your code — leaving a single code path where the flag used to be. ## Notes - Flag evaluation is not exposed over MCP — use the [SDKs](/docs/sdks/overview/) in application code. The MCP server manages flag *configuration*, not runtime evaluation. - `wrap_feature` returns a snippet for you or your agent to apply; it never edits files on its own. - Every mutation is audit-logged and attributed to the token that made it, same as a direct API call. - Requests are rate-limited per token. Errors carry machine-readable hints (`did_you_mean`, `next_actions`) so an agent can recover without human intervention — for example, a typo’d flag key suggests the closest match, and a `403` on a locked action points at what to try next. ## Frequently asked questions ### Do I need a paid plan to use the Featureflip MCP server? No. The MCP server works on every Featureflip plan, including Free. It calls the Management API, which is available on all plans, so you can create an account, generate a token, and let Claude Code or Cursor manage real flags without a subscription or a sales call. ### Which AI tools can manage Featureflip feature flags? Any client that speaks the Model Context Protocol. That covers Claude Code, Cursor, VS Code with Copilot, and Cline, plus autonomous and CI agents. Interactive editor use is best served by a Personal Access Token (`ffp_`) so changes are attributed to you. Unattended agents should use a Service Token (`ffs_`) scoped to one organization. ### Can an AI agent turn on a feature flag in production? Yes, if the token you hand it carries the permission. `toggle_flag` takes an environment, so an agent enables a flag one environment at a time, and every new flag is created disabled everywhere. The tool is marked destructive, which is the signal most MCP clients use to ask you before running it, and each toggle is audit-logged against the token that made it. ### Does the MCP server evaluate flags? No. It manages configuration: creating flags, setting variations, toggling environments, and writing targeting rules. Reading a flag’s value for a given user at runtime belongs to the language [SDKs](/docs/sdks/overview/) , which evaluate locally in memory on your application’s request path. ### What happens when an agent uses a flag key that doesn’t exist? The Management API returns its standard error envelope with a `did_you_mean` field naming the closest matching key, and the server passes that through unchanged. A well-behaved agent reads the suggestion and corrects itself on the next call instead of stopping to ask. --- ## Source: https://featureflip.io/docs/integrations/openfeature/ # OpenFeature [OpenFeature](https://openfeature.dev/) is the CNCF open standard for feature flagging. Featureflip ships providers for the OpenFeature Node.js, .NET, Python, Go, Java and PHP server SDKs, so you can use the vendor-neutral OpenFeature API while Featureflip serves your flags. The Node.js provider is covered first; the .NET , Python , Go , Java and PHP sections follow. ## Installation ```bash npm install @openfeature/server-sdk @featureflip/node @featureflip/openfeature-node ``` Both `@openfeature/server-sdk` and `@featureflip/node` are peer dependencies — your application controls their versions and installs a single copy of each. A single `@featureflip/node` copy is what lets the provider and any direct SDK usage share one underlying client core (see below). ## Quickstart ```ts import { OpenFeature } from '@openfeature/server-sdk'; import { FeatureflipProvider } from '@featureflip/openfeature-node'; await OpenFeature.setProviderAndWait( new FeatureflipProvider({ sdkKey: 'your-server-sdk-key' }), ); const client = OpenFeature.getClient(); const enabled = await client.getBooleanValue( 'new-checkout', false, { targetingKey: 'user-42', plan: 'pro' }, ); ``` The provider owns the underlying Featureflip client. If you also need the raw SDK (for `track()` calls outside OpenFeature, for example), call `FeatureflipClient.get()` with the same SDK key — both handles share one client core. You can also pass an existing `FeatureflipClient` instance to the provider constructor instead of a config. ## Context mapping | OpenFeature context | Featureflip context | | --- | --- | | targetingKey | user_id (used for rollout bucketing) | | Any other attribute | Passed through unchanged | An explicit `user_id` (or `userId`) attribute takes precedence over `targetingKey`. Without either, percentage rollouts bucketed by user serve the control variation — see the SDK’s keyless-context behavior. ## Evaluation reasons | Featureflip outcome | OpenFeature reason | errorCode | | --- | --- | --- | | Targeting rule matched | TARGETING_MATCH | — | | Fallthrough serve | DEFAULT | — | | Flag disabled in environment | DISABLED | — | | Prerequisite not met | PREREQUISITE_FAILED | — | | Flag not found | ERROR | FLAG_NOT_FOUND | | Wrong value type requested | ERROR | TYPE_MISMATCH | | Evaluation error | ERROR | GENERAL | The matched variation key is exposed as `variant`, and rule/prerequisite details are available in `flagMetadata` (`ruleId`, `prerequisiteKey`). ## Tracking OpenFeature tracking calls are forwarded to Featureflip custom events: ```ts client.track('purchase', { targetingKey: 'user-42' }, { value: 9.99 }); ``` ## .NET The `Featureflip.OpenFeature` NuGet package connects the OpenFeature .NET SDK to Featureflip, backed by the `Featureflip.Client` server SDK. ### Installation ```bash dotnet add package OpenFeature dotnet add package Featureflip.Client dotnet add package Featureflip.OpenFeature ``` ### Quickstart ```csharp using OpenFeature; using Featureflip.OpenFeature; await Api.Instance.SetProviderAsync(new FeatureflipProvider("your-server-sdk-key")); var client = Api.Instance.GetClient(); var enabled = await client.GetBooleanValueAsync("new-checkout", false, EvaluationContext.Builder().SetTargetingKey("user-42").Build()); ``` The constructor accepts a server SDK key (with optional `FeatureFlagOptions`) or an existing `IFeatureflipClient`. Context mapping and reasons match the Node provider (`targetingKey` → `user_id`; `Variant`, `ruleId`/`prerequisiteKey` in `FlagMetadata`; `PREREQUISITE_FAILED` for unmet prerequisites). ### .NET limitations - The .NET SDK has no custom-event API, so OpenFeature `Track()` is a no-op. - Object flags accept objects and arrays only; a bare primitive resolves as `TYPE_MISMATCH`. ## Python The `featureflip-openfeature-provider` package connects the OpenFeature Python SDK to Featureflip, backed by the `featureflip` server SDK. ### Installation ```bash pip install featureflip-openfeature-provider ``` ### Quickstart ```python from openfeature import api from openfeature.evaluation_context import EvaluationContext from featureflip_openfeature import FeatureflipProvider api.set_provider_and_wait(FeatureflipProvider(sdk_key="your-server-sdk-key")) client = api.get_client() enabled = client.get_boolean_value( "new-checkout", False, EvaluationContext(targeting_key="user-42", attributes={"plan": "pro"}), ) ``` The constructor takes either an SDK key (with optional `Config`) or an existing `FeatureflipClient` via the `client=` keyword. Context mapping and reasons match the Node provider. `targeting_key` becomes `user_id`, the matched variation is exposed as `variant`, `ruleId` and `prerequisiteKey` are carried in `flag_metadata`, and an unmet prerequisite resolves as `PREREQUISITE_FAILED`. Ownership follows the .NET provider rather than Node: the provider closes the client only when it created one. A client you pass in is a refcounted handle you still hold, so closing it would make your own handle start returning defaults. `FeatureflipClient(...)` blocks on the initial flag fetch. With the `sdk_key` form the provider defers construction to `initialize()`, so that wait happens inside OpenFeature’s provider setup rather than in your constructor. Reach for `set_provider_and_wait` rather than `set_provider`. The plain form runs `initialize()` on a background thread and returns straight away, so flags may not have loaded when the next line evaluates one and you get your default back. This is the Python equivalent of awaiting `setProviderAndWait` in Node. ### Python types Python splits OpenFeature’s numeric accessor into `get_integer_value` and `get_float_value`, which Node does not have: - `get_integer_value` accepts integers and whole-number floats (`1.0`, `1e2`). JSON does not distinguish `1` from `1.0`, so neither can the guard. - `get_float_value` accepts any number, including integral ones. - Both reject booleans. `bool` is a subclass of `int` in Python, so without an explicit exclusion a boolean flag would satisfy `get_integer_value` and return `1`. - `get_object_value` accepts objects and arrays but not strings, even though a `str` is a `Sequence` in Python. Configuration-change events need `featureflip >= 2.7.0`, the release that added the SDK’s update hook. ## Go The `github.com/canopy-labs/featureflip-go-openfeature` module connects the OpenFeature Go SDK to Featureflip, backed by the `featureflip-go` server SDK. ### Installation ```bash go get github.com/canopy-labs/featureflip-go-openfeature ``` ### Quickstart ```go import ( featureflip "github.com/canopy-labs/featureflip-go-openfeature" "github.com/open-feature/go-sdk/openfeature" ) if err := openfeature.SetProviderAndWait(featureflip.NewProvider("your-server-sdk-key")); err != nil { log.Fatal(err) } client := openfeature.NewClient("my-app") enabled, err := client.BooleanValue(ctx, "new-checkout", false, openfeature.NewEvaluationContext("user-42", map[string]any{"plan": "pro"})) ``` `NewProvider` takes an SDK key plus any Featureflip SDK options; `NewProviderWithClient` takes an existing client instead. Ownership follows the .NET and Python providers: `Shutdown` closes the client only when the provider created it. Reach for `SetProviderAndWait` rather than `SetProvider`, for the same reason as in Node and Python — the plain form initializes on a background goroutine and returns before flags have loaded. If you import the Featureflip SDK directly as well, alias one of them. Both packages are named `featureflip`: ```go import ( sdk "github.com/canopy-labs/featureflip-go/v2" featureflip "github.com/canopy-labs/featureflip-go-openfeature" ) ``` ### Go types The OpenFeature Go client spells its boolean read twice, and the difference matters: - `client.Boolean(ctx, key, default, evalCtx)` returns a plain `bool`. - `client.BooleanValue(ctx, key, default, evalCtx)` returns `(bool, error)`. Both resolve identically through the provider. The two-value form surfaces the resolution error; the single-value form swallows it and hands back your default. `IntEvaluation` accepts whole-number JSON values in either form (`1` and `1.0`), since JSON does not distinguish them. Object flags accept objects and arrays only, so a Json flag holding a bare primitive resolves as `TYPE_MISMATCH`. The Featureflip Go SDK resolves the attribute names `userId` and `user_id` from its own user-id field rather than from the attribute map, so the targeting key and both attribute spellings are lifted into it. An explicit `user_id` or `userId` wins over `targetingKey`. ## Java The `io.featureflip:featureflip-openfeature` artifact connects the OpenFeature Java SDK to Featureflip, backed by the `featureflip-java` server SDK. ### Installation ```groovy dependencies { implementation 'io.featureflip:featureflip-openfeature:0.1.0' } ``` Or with Maven: ```xml io.featureflip featureflip-openfeature 0.1.0 ```  io.featureflip featureflip-openfeature 0.1.0"> Both SDKs come along as transitive dependencies. Java 11 or higher. ### Quickstart ```java import dev.openfeature.sdk.*; import io.featureflip.openfeature.FeatureflipProvider; OpenFeatureAPI api = OpenFeatureAPI.getInstance(); api.setProviderAndWait(new FeatureflipProvider("your-server-sdk-key")); Client client = api.getClient(); boolean enabled = client.getBooleanValue("new-checkout", false, new ImmutableContext("user-42", Map.of("plan", new Value("pro")))); ``` The constructor takes an SDK key, optionally with a `FeatureFlagConfig`; a third form takes an existing `FeatureflipClient`. Ownership follows the .NET, Python and Go providers: `shutdown()` closes the client only when the provider created it. Reach for `setProviderAndWait` rather than `setProvider`, for the same reason as in the other runtimes — the plain form returns before flags have loaded. ### Java types `getIntegerEvaluation` accepts whole-number JSON values in either form (`1` and `1.0`), since JSON does not distinguish them. Object flags accept objects and arrays only, so a Json flag holding a bare primitive resolves as `TYPE_MISMATCH`. A wrong-typed flag is reported as `TYPE_MISMATCH` distinctly from a genuine evaluation failure, which is `GENERAL`. The provider reads values untyped and applies the type check itself to keep those apart — the SDK’s typed accessors report both as an error. The Featureflip Java SDK resolves the attribute names `userId` and `user_id` from its own user-id field rather than from the attribute map, so the targeting key and both attribute spellings are lifted into it. An explicit `user_id` or `userId` wins over `targetingKey`. A numeric identity is rendered without a decimal part, so an id sent as a number buckets identically to the same id sent as a string. A failed initial flag load does not fail `setProviderAndWait`. The SDK keeps retrying in the background and evaluations serve your defaults until flags arrive, so a transient outage at startup degrades rather than takes the provider down. ## PHP The `featureflip/featureflip-openfeature` Packagist package connects the OpenFeature PHP SDK to Featureflip, backed by the `featureflip/featureflip-php` server SDK. ### Installation ```bash composer require featureflip/featureflip-openfeature ``` ### Quickstart ```php use Featureflip\FeatureflipClient; use Featureflip\Config; use Featureflip\OpenFeature\FeatureflipProvider; use OpenFeature\OpenFeatureAPI; $client = FeatureflipClient::get('your-server-sdk-key', new Config( cache: $psrCache, httpClient: $psrHttpClient, requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory, )); OpenFeatureAPI::getInstance()->setProvider(new FeatureflipProvider($client)); $enabled = OpenFeatureAPI::getInstance() ->getClient() ->getBooleanValue('new-checkout', false, $context); ``` setProvider(new FeatureflipProvider($client));$enabled = OpenFeatureAPI::getInstance() ->getClient() ->getBooleanValue('new-checkout', false, $context);"> Unlike the other five providers on this page, which all accept a server SDK key directly, there is no SDK-key constructor here: build a `FeatureflipClient` yourself and pass it in. The PHP SDK’s core hard-requires caller-supplied PSR-18 (HTTP client), PSR-17 (request/stream factory) and PSR-16 (cache) implementations and throws without them, so a key-only constructor would have to choose an HTTP client and a cache on the application’s behalf. Passing the client in is also the idiomatic PHP shape: it is a service-container object, and an application evaluating flags through both the SDK directly and through OpenFeature should share one. The provider never closes the client it is given; the caller owns it and is responsible for calling `$client->close()` at shutdown. Reasons and variants match the other providers: the matched variation is exposed as `variant`, and an unmet prerequisite resolves as `PREREQUISITE_FAILED`. The targeting key is written into the Featureflip context under both `user_id` and `userId`, because the PHP SDK resolves a condition’s attribute by exact key with no aliasing between the two spellings — writing both is what lets a targeting rule against either one resolve. An explicit `user_id` or `userId` attribute still takes precedence and is passed through exactly as you supplied it. ### PHP types - `resolveIntegerValue` accepts integers and whole-number floats (`1.0`), since JSON does not distinguish them. A matched value is narrowed to a real `int` before being returned, because the OpenFeature client validates the resolved value’s type and would otherwise return your default with reason `ERROR` and error code `GENERAL` — silently, rather than raising. - `resolveFloatValue` accepts any JSON number, integers included. - `resolveObjectValue` accepts objects and arrays only — not strings, even though a `str` is treated as a sequence in some SDKs’ equivalent accessor. - A `DateTime`-valued context attribute is converted to an ISO-8601 string before evaluation, recursing into nested arrays. Every SDK gates its date-comparison rules on the ISO grammar, so a `DateTime` object left unconverted would stringify unpredictably (or not at all) and silently match no date rule. ### PHP limitations - **No provider events.** The `open-feature/sdk` PHP package has no provider events API at all — no `PROVIDER_READY`/`PROVIDER_CONFIGURATION_CHANGED` equivalent to subscribe to, unlike the other five providers on this page. Flag changes are therefore picked up passively, on the underlying SDK’s poll interval, rather than pushed to your application. - **No flag metadata at this version.** The released `open-feature/sdk` (2.3.0) has no flag-metadata API, so `ruleId` and `prerequisiteKey` are not exposed. Requires `featureflip/featureflip-php` 3.3.0 or later. ## Reacting to flag changes The Node provider emits OpenFeature’s `PROVIDER_CONFIGURATION_CHANGED` whenever flag configuration changes after startup. The event carries `flagsChanged` — the keys of the flags affected by that change, batched into a single event: ```javascript import { OpenFeature, ProviderEvents } from "@openfeature/server-sdk"; OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, (details) => { console.log("flags changed:", details?.flagsChanged); }); ``` { console.log("flags changed:", details?.flagsChanged);});"> The .NET provider emits the same event, from version 0.2.0: ```csharp using OpenFeature; using OpenFeature.Constant; Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, details => { Console.WriteLine($"flags changed: {string.Join(", ", details.FlagsChanged ?? [])}"); }); ``` { Console.WriteLine($"flags changed: {string.Join(", ", details.FlagsChanged ?? [])}");});"> The Python provider emits it too: ```python from openfeature import api from openfeature.event import ProviderEvent api.add_handler( ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, lambda details: print("flags changed:", details.flags_changed), ) ``` The Go provider emits it too, from version 0.2.0: ```go callback := func(details openfeature.EventDetails) { log.Printf("flags changed: %v", details.FlagChanges) } openfeature.AddHandler(openfeature.ProviderConfigChange, &callback) ``` The Java provider emits it too: ```java client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details -> System.out.println("flags changed: " + details.getFlagsChanged())); ```  System.out.println("flags changed: " + details.getFlagsChanged()));"> A flag is reported when it is created, deleted, redefined, or when a segment its targeting rules reference changes. A flag is also reported when a flag it lists as a prerequisite changes, since that alters what it evaluates to. The initial flag load does not fire the event — that is signaled by `PROVIDER_READY`. ## Limitations - `getObjectValue` accepts objects and arrays only: a Json flag holding a bare primitive (string/number/boolean) resolves as `TYPE_MISMATCH` and returns the default. - Node.js, .NET, Python, Go, Java and PHP have OpenFeature providers; other SDKs are in progress. - The PHP provider has no provider-events API to subscribe to (the `open-feature/sdk` PHP package doesn’t expose one), so flag changes there arrive on the underlying SDK’s poll interval rather than being pushed to your application. --- ## Source: https://featureflip.io/docs/integrations/terraform/ # Terraform Provider The Featureflip Terraform provider manages your flag configuration as code through the [Management API](/docs/management-api/overview/) : projects, environments, feature flags (including variations and per-environment targeting), user segments, SDK keys, and [outbound webhooks](/docs/guides/webhooks/) . It is published as `canopy-labs/featureflip` on both the [Terraform Registry](https://registry.terraform.io/providers/canopy-labs/featureflip/latest) and the [OpenTofu Registry](https://search.opentofu.org/provider/canopy-labs/featureflip/latest) , and developed in the open at [canopy-labs/terraform-provider-featureflip](https://github.com/canopy-labs/terraform-provider-featureflip) . ## Installation ```hcl terraform { required_providers { featureflip = { source = "canopy-labs/featureflip" version = "~> 0.1" } } } provider "featureflip" { organization = "acme-corp" # token via FEATUREFLIP_TOKEN environment variable (recommended), # or: token = var.featureflip_token } ``` 0.1" } }}provider "featureflip" { organization = "acme-corp" # token via FEATUREFLIP_TOKEN environment variable (recommended), # or: token = var.featureflip_token}"> ## OpenTofu The installation block above works with OpenTofu as written. Run `tofu init` and OpenTofu fetches `canopy-labs/featureflip` from its own registry (the same release files the Terraform Registry serves) and verifies the download against the Canopy Labs signing key. Wherever this page says `terraform`, type `tofu` and keep the same arguments, `tofu import` included. ## Authentication Create an API token in the Featureflip dashboard — tokens cannot be created through the public API: - **Service token (ffs_…) — recommended.** Organization Settings → Service Tokens. Scoped to one organization with a role and an optional project allowlist. Use Member or Admin. Viewer tokens cannot write, and webhooks need Admin. - **Personal access token (ffp_…).** Acts as your user across your organizations. Prefer the `FEATUREFLIP_TOKEN` environment variable over putting the token in configuration. `organization` and `base_url` can likewise be set via `FEATUREFLIP_ORGANIZATION` and `FEATUREFLIP_BASE_URL`. ## Quickstart ```hcl resource "featureflip_project" "web" { key = "web" name = "Web App" description = "Customer-facing web application" } # Boolean flags get true/false variations automatically. resource "featureflip_feature_flag" "new_checkout" { project = featureflip_project.web.key key = "new-checkout" name = "New Checkout Flow" type = "Boolean" tags = ["checkout"] } # Non-Boolean flags declare their variations inline. resource "featureflip_feature_flag" "banner_text" { project = featureflip_project.web.key key = "banner-text" name = "Banner Text" type = "String" variations = [ { key = "control", name = "Control", value = "Welcome!" }, { key = "variant", name = "Variant", value = "Hi there!" }, ] } ``` ## Targeting rules `featureflip_flag_environment` manages a flag’s state and targeting in a single environment. List position is rule priority: ```hcl resource "featureflip_flag_environment" "new_checkout_prod" { project = featureflip_project.web.key flag = featureflip_feature_flag.new_checkout.key environment = "production" enabled = true default_variation = "false" rules = [ { description = "Beta users always get the new checkout" variation = "true" segment = featureflip_segment.beta_users.key }, { description = "50% rollout in Germany" variation = "true" rollout_percentage = 50 condition_groups = [ { operator = "And" conditions = [ { attribute = "country", operator = "Equals", values = ["DE"] }, ] }, ] }, ] } ``` ## Webhooks `featureflip_webhook` subscribes an HTTPS endpoint to configuration changes, so the receiver that reacts to a toggle is declared in the same place as the flag. It needs provider 0.3.0 or later and an Admin token: ```hcl resource "featureflip_webhook" "deploys" { name = "Deploy pipeline" url = "https://ci.example.com/hooks/featureflip" event_types = ["flag.toggled", "flag.environment_config.updated"] project_ids = [featureflip_project.web.id] environment_ids = [featureflip_environment.production.id] } output "webhook_signing_secret" { value = featureflip_webhook.deploys.secret sensitive = true } ``` Leave out `event_types`, `project_ids` or `environment_ids` and the subscription covers everything in that dimension. Scopes take ids rather than keys, which is why the example passes `.id`. The `featureflip_webhook_event_types` data source returns the full list of event types if you want to build a filter from it. To rotate the signing secret, add a `featureflip_webhook_secret`. Featureflip signs every delivery with every active secret, so your receiver can switch to the new one while the old one still works. When it has, bring the original secret under Terraform with an `import` block pointing at the webhook’s `secret_id`, then delete that resource in a later apply to retire it. The [registry docs](https://registry.terraform.io/providers/canopy-labs/featureflip/latest/docs/resources/webhook_secret) walk through the three steps. ## Resources and data sources | Resource | Manages | | --- | --- | | featureflip_project | A project | | featureflip_environment | An environment within a project | | featureflip_feature_flag | A flag and its variations | | featureflip_flag_environment | A flag’s per-environment state and targeting rules | | featureflip_segment | A reusable user segment | | featureflip_sdk_key | An SDK key (destroying it revokes the key) | | featureflip_webhook | An outbound webhook subscription | | featureflip_webhook_secret | An extra signing secret on a webhook, for rotation (destroying it retires the secret) | Data sources exist for reading an existing `featureflip_organization`, `featureflip_project`, `featureflip_environment`, `featureflip_feature_flag`, and `featureflip_segment`. `featureflip_webhook_event_types` lists the event types a webhook can subscribe to. Every resource supports `terraform import`, so you can adopt flags that already exist: ```bash terraform import featureflip_feature_flag.new_checkout web/new-checkout ``` ## Things to know before you apply **It manages flag configuration, not flag state.** The provider owns which flags exist, their variations, and their targeting rules. It is not built for flipping a flag on and off during an incident: that is a dashboard or API action, and a Terraform apply is a slower path to it. A common pattern is to manage projects, environments, flags, and segments in Terraform while leaving day-to-day toggling to the dashboard. **rules is authoritative.** Once you set `rules` on a `featureflip_flag_environment`, Terraform owns every rule in that flag-environment, and rules added in the dashboard are removed on the next apply. Destroying the resource deletes the managed rules and disables the flag in that environment, leaving the fallthrough configuration in place. **SDK keys are sensitive, and destroying one revokes it.** The plaintext value of a `featureflip_sdk_key` is only available at creation and is stored in Terraform state, so protect your state. Destroying the resource revokes the key, which cannot be undone. Imported keys cannot recover their plaintext. **Webhook signing secrets follow the same rule.** The secret a webhook is created with, and each `featureflip_webhook_secret`, is readable once and then kept only in your state. A subscription always keeps at least one active secret. If removing a `featureflip_webhook_secret` would retire the last one, the provider leaves it active, drops it from state and says so in a warning. **An auto-disabled webhook comes back on at the next apply.** Featureflip switches a subscription off after 20 failed delivery rounds in a row. Because `enabled` defaults to `true`, the next plan shows it changing back, and applying turns deliveries on again. Fix the receiver before you apply, or set `enabled = false` until you have. ## Reference Full resource and data-source documentation, including every argument and attribute, is published on the [Terraform Registry](https://registry.terraform.io/providers/canopy-labs/featureflip/latest/docs) . For a worked walkthrough covering install, a first project and flag, importing flags you already have, and where to stop putting flag config in code, see [Feature Flags as Code: Managing Featureflip with Terraform](/blog/feature-flags-as-code-terraform/) . For the process around the provider, including what to look for when reviewing a flag diff, how to structure the plan and apply jobs, and how to handle configuration drift after an incident, see [GitOps for feature flags](/blog/gitops-feature-flags/) . --- ## Source: https://featureflip.io/docs/management-api/authentication/ # Authentication The Management API authenticates with a bearer token in the `Authorization` header: ```plaintext Authorization: Bearer ``` "> There are two token types. Both are created in the Featureflip dashboard and are shown **once** at creation — store them securely. ## Personal Access Tokens (ffp_) A Personal Access Token acts as **you**: it carries your identity and your role in every organization you belong to. Use one for personal scripting, local tooling, or exploration. Create one under **Settings → API Tokens → Personal Access Tokens**. A personal token is prefixed `ffp_`. ## Service Tokens (ffs_) A Service Token is a **machine identity** scoped to a single organization. It carries an explicit role and, optionally, an allowlist of projects it may access. Use one for CI, automation, or shared infrastructure — it is not tied to any person and keeps working after team changes. Create one under **Organization Settings → Service Tokens**. A service token is prefixed `ffs_`. ## Roles and permissions Both token types act with a role. Roles are hierarchical — `Owner` is most privileged, `Viewer` least: | Role | Can do | | --- | --- | | Owner | Everything, including org settings | | Admin | Manage projects, flags, members | | Member | Create and edit flags, targeting, segments | | Viewer | Read-only | A write performed by a token below the required role returns `403 forbidden`. A service token whose project allowlist excludes the target project returns `404 not_found` for that project. ## Token expiry and errors Tokens may have an expiry. A missing, malformed, expired, or revoked token returns `401 unauthorized`: ```json { "error": "unauthorized", "message": "Missing or invalid API token.", "docs_url": "https://featureflip.io/docs/management-api/errors/unauthorized" } ``` See [Errors](/docs/management-api/errors/) for the full error contract. --- ## Source: https://featureflip.io/docs/management-api/conventions/ # Conventions These conventions apply to every Management API endpoint. ## Versioning All endpoints live under `/api/v1`. Breaking changes ship under a new version prefix; additive changes (new fields, new endpoints) may appear within `v1`. ## Content type Send request bodies as JSON with `Content-Type: application/json`. Requests with another content type are rejected with `415 unsupported_media_type`. Responses are always JSON. ## Resource addressing Organizations, projects, flags, environments, and segments are addressed by their **key or slug** — the same stable identifier you see in the dashboard: ```plaintext GET /api/v1/orgs/{org}/projects/{project}/flags/{flag} ``` Nested resources without a user-facing key — targeting rules and variations — are addressed by their **GUID** `id`, returned when you create or list them. ## Pagination List endpoints are cursor-paginated. Pass an opaque `cursor` query parameter to fetch the next page: ```plaintext GET /api/v1/orgs/{org}/projects/{project}/flags?cursor= ``` "> The response wraps items with the next cursor: ```json { "items": [ /* … */ ], "next_cursor": "eyJvIjoyNX0" } ``` When `next_cursor` is `null` there are no more pages. Because cursors are offset-based, the final “next” page can legitimately come back empty — treat an empty page as the end. ## Errors Every error response uses one frozen JSON envelope: ```json { "error": "validation_failed", "message": "One or more fields are invalid.", "docs_url": "https://featureflip.io/docs/management-api/errors/validation_failed", "fields": { "key": ["Key must be lowercase kebab-case."] } } ``` | Field | Type | Always present | Description | | --- | --- | --- | --- | | error | string | Yes | Stable machine-readable code (see Errors) | | message | string | Yes | Human-readable explanation | | docs_url | string | Yes | Link to the docs page for this error code | | fields | object | No | Per-field validation messages, present on validation_failed | | retry_after | integer | No | Seconds to wait, present on rate_limited | | did_you_mean | array | No | Suggested corrections for an unknown key/slug | | next_actions | array | No | Suggested follow-up requests ({ "method", "path" }) | See [Errors](/docs/management-api/errors/) for every code and how to resolve it. ## Rate limiting Every response carries rate-limit headers: | Header | Description | | --- | --- | | X-RateLimit-Limit | Requests allowed in the current window | | X-RateLimit-Remaining | Requests remaining in the window | | X-RateLimit-Reset | Unix time (seconds) when the window resets | Exceeding the limit returns `429 rate_limited` with a `retry_after` field and a `Retry-After` header. Back off for that many seconds before retrying. ## Idempotency `POST` requests that create a resource accept an `Idempotency-Key` header: ```plaintext Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff ``` Send a unique key (e.g. a UUID) per logical create. If a request with the same token and key is retried, the original result is replayed instead of creating a duplicate. If a second request with the same key arrives while the first is still in flight, it returns `409 idempotency_key_in_progress` — retry after the first completes. ## Hypermedia actions (_actions) Every resource response includes an `_actions` object describing which follow-up operations the current token may perform on that resource, and why not when it can’t: ```json "_actions": { "update": { "allowed": true }, "delete": { "allowed": false, "reason": "requires Member role" } } ``` Use `_actions` to drive UI affordances without hardcoding the permission model. --- ## Source: https://featureflip.io/docs/management-api/errors/ # Errors Every Management API error returns the same JSON envelope. The `error` field is a stable, machine-readable code; `docs_url` links to the page for that code. ```json { "error": "not_found", "message": "Project 'checkout' was not found.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found" } ``` See [Conventions → Errors](/docs/management-api/conventions/#errors) for every envelope field. ## Error codes | Code | HTTP | Meaning | | --- | --- | --- | | bad_request | 400 | Malformed or unprocessable request | | validation_failed | 400 | One or more fields failed validation | | unauthorized | 401 | Missing, invalid, expired, or revoked token | | forbidden | 403 | Token’s role is insufficient | | sso_required | 403 | The organization requires single sign-on and the token didn’t come through it | | not_found | 404 | Resource doesn’t exist or isn’t visible | | method_not_allowed | 405 | HTTP method not supported on this path | | idempotency_key_in_progress | 409 | A same-key create is still in flight | | unsupported_media_type | 415 | Content-Type was not application/json | | rate_limited | 429 | Rate limit exceeded | | internal_error | 500 | Unexpected server error | --- ## Source: https://featureflip.io/docs/management-api/errors/bad_request/ # bad_request **HTTP status:** `400 Bad Request` The request was malformed or otherwise couldn’t be processed — distinct from [`validation_failed`](/docs/management-api/errors/validation_failed/) , which reports specific invalid fields. ## Common causes - Malformed JSON in the request body. - A field with the wrong JSON type. - A malformed pagination `cursor`. ## How to fix - Validate your JSON and field types against the [API Reference](/docs/management-api/reference/) . - Pass pagination cursors back exactly as received. ## Example ```json { "error": "bad_request", "message": "The request body could not be parsed.", "docs_url": "https://featureflip.io/docs/management-api/errors/bad_request" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/forbidden/ # forbidden **HTTP status:** `403 Forbidden` The token is valid but not allowed to perform this action. ## Common causes - The token’s role is below what the action requires (e.g. a Viewer attempting a write). - A Service Token acting outside its granted permissions. ## How to fix Use a token with a sufficient [role](/docs/management-api/authentication/#roles-and-permissions) , or adjust the token’s role in the dashboard. Inspect a resource’s `_actions` block to see which operations your token may perform. ## Example ```json { "error": "forbidden", "message": "This action requires the Member role.", "docs_url": "https://featureflip.io/docs/management-api/errors/forbidden" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/idempotency_key_in_progress/ # idempotency_key_in_progress **HTTP status:** `409 Conflict` Another request with the same `Idempotency-Key` is still being processed. ## Common causes - A client retried a create before the original completed. - Two concurrent requests reused the same idempotency key. ## How to fix Wait for the original request to finish, then retry. Use a unique key per logical create. See [Conventions → Idempotency](/docs/management-api/conventions/#idempotency) . ## Example ```json { "error": "idempotency_key_in_progress", "message": "A request with this Idempotency-Key is already in progress.", "docs_url": "https://featureflip.io/docs/management-api/errors/idempotency_key_in_progress" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/internal_error/ # internal_error **HTTP status:** `500 Internal Server Error` An unexpected error occurred on our side. These are not caused by your request. ## How to fix Retry after a short delay. If it persists, contact support with the time of the request. ## Example ```json { "error": "internal_error", "message": "An unexpected error occurred.", "docs_url": "https://featureflip.io/docs/management-api/errors/internal_error" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/method_not_allowed/ # method_not_allowed **HTTP status:** `405 Method Not Allowed` The path exists but doesn’t support the HTTP method you used. ## Common causes - Using `PUT` where the endpoint expects `POST` (or vice versa). - Calling a collection action with the wrong verb. ## How to fix Check the method for the endpoint in the [API Reference](/docs/management-api/reference/) . ## Example ```json { "error": "method_not_allowed", "message": "The GET method is not supported on this path.", "docs_url": "https://featureflip.io/docs/management-api/errors/method_not_allowed" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/not_found/ # not_found **HTTP status:** `404 Not Found` The addressed resource doesn’t exist, or isn’t visible to this token. ## Common causes - A misspelled org, project, flag, environment, or segment key. - A Service Token whose project allowlist excludes the target project. - A resource that was deleted. ## How to fix Check the key/slug in the path. If a `did_you_mean` array is present, try one of its suggestions. Confirm the token’s project allowlist includes the project. ## Example ```json { "error": "not_found", "message": "Flag 'new-chekout' was not found.", "docs_url": "https://featureflip.io/docs/management-api/errors/not_found", "did_you_mean": ["new-checkout"] } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/rate_limited/ # rate_limited **HTTP status:** `429 Too Many Requests` You’ve exceeded the rate limit for the current window. ## Common causes - Bursting many requests in a short window. - Multiple processes sharing one token. ## How to fix Back off for the number of seconds in `retry_after` (also sent as the `Retry-After` header), then retry. Watch the `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers. See [Conventions → Rate limiting](/docs/management-api/conventions/#rate-limiting) . ## Example ```json { "error": "rate_limited", "message": "Rate limit exceeded.", "docs_url": "https://featureflip.io/docs/management-api/errors/rate_limited", "retry_after": 30 } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/sso_required/ # sso_required **HTTP status:** `403 Forbidden` The organization requires [single sign-on](/docs/guides/single-sign-on/) , and the token on this request wasn’t created through its identity provider. The response carries a `connection_id` naming the SSO connection to sign in with. This is separate from [`forbidden`](/docs/management-api/errors/forbidden/) , which means the token’s role can’t perform the action. ## Common causes - The personal access token was created during a password, Google or GitHub session, and an Owner has since turned on **Require single sign-on**. - The token belongs to a different organization’s SSO session. - The token was created during an SSO session, and an Owner has since deactivated the connection or changed its issuer or client ID. ## How to fix - Sign in to Featureflip with **Continue with SSO**, create a new personal access token, and use that one. - Retire the old token once the new one works. ## Example ```json { "error": "sso_required", "message": "This organization requires single sign-on.", "connection_id": "3f2b8c1e-5d4a-4e7b-9a61-0c2d8e4f7a95", "docs_url": "https://featureflip.io/docs/management-api/errors/sso_required" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/unauthorized/ # unauthorized **HTTP status:** `401 Unauthorized` The request had no valid bearer token. ## Common causes - Missing `Authorization` header. - A malformed, expired, or revoked token. - Using an Evaluation API SDK key here (the Management API needs a `ffp_`/`ffs_` token). ## How to fix Send `Authorization: Bearer ` with a current Personal Access Token or Service Token. See [Authentication](/docs/management-api/authentication/) . ## Example ```json { "error": "unauthorized", "message": "Missing or invalid API token.", "docs_url": "https://featureflip.io/docs/management-api/errors/unauthorized" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/unsupported_media_type/ # unsupported_media_type **HTTP status:** `415 Unsupported Media Type` The request body wasn’t sent as JSON. ## Common causes - A missing or non-`application/json` `Content-Type` header on a `POST`/`PUT`. ## How to fix Send `Content-Type: application/json` and a JSON body. ## Example ```json { "error": "unsupported_media_type", "message": "Content-Type must be application/json.", "docs_url": "https://featureflip.io/docs/management-api/errors/unsupported_media_type" } ``` --- ## Source: https://featureflip.io/docs/management-api/errors/validation_failed/ # validation_failed **HTTP status:** `400 Bad Request` One or more fields failed validation. The `fields` object maps each invalid field to its messages. ## Common causes - A key that isn’t lowercase kebab-case. - A missing required field. - A value outside its allowed set (e.g. an unknown flag `type`). ## How to fix Read the `fields` object and correct each listed field. See the [API Reference](/docs/management-api/reference/) for each field’s rules. ## Example ```json { "error": "validation_failed", "message": "One or more fields are invalid.", "docs_url": "https://featureflip.io/docs/management-api/errors/validation_failed", "fields": { "key": ["Key must be lowercase kebab-case."] } } ``` --- ## Source: https://featureflip.io/docs/management-api/overview/ # Management API Overview The **Management API** is the REST interface for configuring Featureflip programmatically: creating and updating projects, environments, feature flags, variations, targeting rules, segments, and SDK keys. It is the automation counterpart to the dashboard. If you only need to **evaluate** flags in your application, use an [SDK](/docs/sdks/overview/) or the [Evaluation API](/docs/api/overview/) instead — those are the high-throughput serving paths. Use the Management API to script configuration: seed flags in CI, sync environments, or build internal tooling. Building with AI agents? The [MCP server](/docs/integrations/mcp/) exposes this API as tools for Claude Code, Cursor, and other MCP clients. ## Base URL ```plaintext https://api.featureflip.io ``` All Management API paths are under `/api/v1` and are relative to this base URL. ## Authentication Every request is authenticated with a bearer token — a Personal Access Token or a Service Token: ```plaintext Authorization: Bearer ``` "> See [Authentication](/docs/management-api/authentication/) for how to create tokens and choose between them. ## What you can manage | Resource | Description | | --- | --- | | Organizations | Your organizations and their metadata | | Projects | Projects within an organization | | Environments | Environments within a project | | Feature flags | Flags, variations, archive/restore | | Targeting | Per-environment enablement, rules, and rollout | | Segments | Reusable user segments | | SDK keys | Environment-scoped keys for the Evaluation API | ## Next steps - [Authentication](/docs/management-api/authentication/) — create a token - [Quickstart](/docs/management-api/quickstart/) — create your first flag end to end - [Conventions](/docs/management-api/conventions/) — pagination, errors, rate limits, idempotency - [API Reference](/docs/management-api/reference/) — every endpoint, request, and response --- ## Source: https://featureflip.io/docs/management-api/quickstart/ # Quickstart This walkthrough creates a project, an environment, a flag, enables it, and adds a targeting rule. It assumes you have a [token](/docs/management-api/authentication/) . Every request needs `Authorization: Bearer `; every body is `Content-Type: application/json`. The base URL is `https://api.featureflip.io`. ## 1. Find your organization List the organizations your token can access: ```plaintext GET /api/v1/orgs ``` Note the `key` of the org you want to work in — you’ll use it as `{org}` below. You can also confirm who your token authenticates as with `GET /api/v1/me`. ## 2. Create a project ```plaintext POST /api/v1/orgs/{org}/projects ``` ```json { "key": "checkout", "name": "Checkout", "description": "Checkout flow experiments" } ``` Response `201 Created`: ```json { "id": "0d2b…", "key": "checkout", "name": "Checkout", "description": "Checkout flow experiments", "createdAt": "2026-07-08T12:00:00Z", "updatedAt": "2026-07-08T12:00:00Z", "_actions": { "update": { "allowed": true }, "delete": { "allowed": true } } } ``` ## 3. Create an environment ```plaintext POST /api/v1/orgs/{org}/projects/checkout/environments ``` ```json { "key": "production", "name": "Production" } ``` ## 4. Create a feature flag ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags ``` ```json { "key": "new-checkout", "name": "New checkout", "type": "Boolean", "description": "Rolls out the redesigned checkout", "clientSideVisible": false } ``` Response `201 Created` includes the flag’s `variations` (for a Boolean flag, the generated `on`/`off` variations) and an `_actions` block. A new flag is **off** in every environment until you enable it. ## 5. Enable the flag in an environment ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags/new-checkout/environments/production/toggle ``` ```json { "enabled": true } ``` The toggle returns `204 No Content`. To see the resulting configuration, fetch the flag’s environment config: ```plaintext GET /api/v1/orgs/{org}/projects/checkout/flags/new-checkout/environments/production ``` Response `200 OK`: ```json { "environmentId": "a1c9…", "environmentKey": "production", "isEnabled": true, "defaultVariationId": "…", "strategy": "SingleVariation", "prerequisites": [], "_actions": { "update": { "allowed": true } } } ``` ## 6. Add a targeting rule Serve the flag to a subset of users. This rule targets a segment; you can instead pass `conditionGroups` for attribute-based targeting or `rolloutPercentage` for a percentage rollout. ```plaintext POST /api/v1/orgs/{org}/projects/checkout/flags/new-checkout/environments/production/rules ``` ```json { "description": "Beta users", "variationId": "", "userSegmentId": "", "conditionGroups": [] } ``` ", "userSegmentId": "", "conditionGroups": []}"> Response `201 Created`: ```json { "id": "7f3a…", "priority": 0, "description": "Beta users", "variationId": "", "userSegmentId": "", "rolloutPercentage": null, "conditionGroups": [], "_actions": { "update": { "allowed": true }, "delete": { "allowed": true } } } ``` ", "userSegmentId": "", "rolloutPercentage": null, "conditionGroups": [], "_actions": { "update": { "allowed": true }, "delete": { "allowed": true } }}"> ## Next steps - Browse every endpoint in the [API Reference](/docs/management-api/reference/) . - Review [Conventions](/docs/management-api/conventions/) for pagination, rate limits, and idempotency. - Evaluate the flag in your app with an [SDK](/docs/sdks/overview/) . --- ## Source: https://featureflip.io/docs/quickstart/android/ # Android Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in an Android application. The SDK is a client-side SDK — flags are evaluated server-side and only values are returned. Looking for the full API? The [Android SDK reference](/docs/sdks/android/) covers every method, every option, and every type — including coroutine integration, Compose helpers, and lifecycle behavior. ## Prerequisites - A Featureflip account with at least one feature flag created - A **client** SDK key from your environment settings - Flags you want to use must have **Client-side visible** enabled - Android API 21+ / Kotlin 1.9+ / Java 17+ ## Install Add the dependency to your `build.gradle.kts`: ```kotlin dependencies { implementation("io.featureflip:featureflip-android:2.3.0") } ``` Or with Groovy DSL (`build.gradle`): ```groovy dependencies { implementation 'io.featureflip:featureflip-android:2.3.0' } ``` ## Initialize the client ```kotlin import dev.featureflip.android.FeatureflipClient import dev.featureflip.android.FeatureflipConfig val config = FeatureflipConfig( clientKey = "your-client-sdk-key", context = mapOf("user_id" to "user-123"), ) val client = FeatureflipClient.get(config) client.initialize() ``` `FeatureflipClient.get()` is the only way to obtain a client — the public constructor was removed in v2.0. The factory dedupes by client key, so calling `get()` multiple times with the same key returns handles pointing at one shared underlying client. This makes the SDK safe to call from per-Activity or per-ViewModel constructors without leaking SSE connections. See [Lifetime](/docs/sdks/android/#lifetime) in the reference for details. `initialize()` loads cached flags from disk, fetches fresh values from the evaluation API, and starts SSE streaming for real-time updates. Call from a background thread to avoid blocking the UI. ## Evaluate a flag ```kotlin val showBanner = client.boolVariation("new-banner", false) if (showBanner) { println("Showing the new banner") } else { println("Using the default experience") } ``` The second parameter is the default value, returned when the flag is missing or has an unexpected type. Other variation methods are available for different value types: ```kotlin val color = client.stringVariation("banner-color", "blue") val limit = client.numberVariation("rate-limit", 100.0) ``` ## Clean up Close the client when your app is shutting down: ```kotlin client.close() ``` The SDK automatically pauses streaming when the app enters the background and resumes on foreground. ## How it works The Android SDK is a **client-side SDK** — flags are evaluated on the Featureflip server, not locally in your app. On initialization, the SDK sends the user context to the evaluation API and receives pre-evaluated flag values. It then opens an SSE (Server-Sent Events) connection that pushes updates in real-time whenever flags change in the dashboard. Flag values are cached to disk, so your app has working values immediately on launch while the SDK fetches fresh data in the background. When the app moves to the background, the SDK pauses the SSE connection to conserve battery and resumes it when the app returns to the foreground. ## Troubleshooting **initialize() blocks the UI thread** `initialize()` performs network requests and should be called from a background thread or coroutine. Use `kotlinx.coroutines` to call it from an IO dispatcher: `withContext(Dispatchers.IO) { client.initialize() }`. On the main thread, it can cause ANR (Application Not Responding) errors. **Flag values are always defaults** Verify you are using a **client** SDK key (not a server SDK key). Client and server keys are different — using the wrong type results in authentication errors or empty responses. Also confirm that the flags you want to access have **Client-side visible** enabled in the dashboard. **Gradle sync fails after adding the dependency** The SDK requires Android API 21+ and Java 17+. Check your `build.gradle.kts` has `minSdk = 21` and your JDK version is 17 or later. If using an older JDK, update it in Android Studio’s settings under **Build → Build Tools → Gradle → Gradle JDK**. ## Next steps - [Android SDK reference](/docs/sdks/android/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/csharp/ # C# Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a C# application. The SDK targets .NET 8.0, .NET 10.0, and .NET Standard 2.0, so it works with .NET 8+, .NET Framework 4.6.1+, and .NET Core 2.0+. Looking for the full API? The [C# SDK reference](/docs/sdks/csharp/) covers every option, dependency injection registration, and API surface — including singleton lifetime, multi-targeting notes, and `EvaluationContext` details. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - .NET 8.0 SDK or later (also supports .NET Standard 2.0 for .NET Framework 4.6.1+) ## Install ```bash dotnet add package Featureflip.Client ``` ## Initialize the client ```csharp using Featureflip.Client; var options = new FeatureFlagOptions { BaseUrl = "https://eval.featureflip.io", WaitForInitialization = true }; using var client = FeatureflipClient.Get("your-sdk-key", options); ``` Setting `WaitForInitialization = true` causes the constructor to block until the initial flag configuration is fetched. The client throws `FeatureFlagInitializationException` if initialization fails. If you prefer, set the `FEATUREFLIP_SDK_KEY` environment variable and omit the `sdkKey` parameter: ```csharp using var client = FeatureflipClient.Get(options: options); ``` ## Evaluate a flag ```csharp var context = new EvaluationContext { UserId = "user-123" }; bool showBanner = client.BoolVariation("new-banner", context, defaultValue: false); if (showBanner) { Console.WriteLine("Showing the new banner"); } else { Console.WriteLine("Using the default experience"); } ``` The `defaultValue` argument is returned when the flag is not found or evaluation fails. Other variation methods are available for different value types: ```csharp string color = client.StringVariation("banner-color", context, defaultValue: "blue"); double limit = client.DoubleVariation("rate-limit", context, defaultValue: 100.0); ``` ## Clean up The client implements `IDisposable`. The `using` statement in the initialization example ensures the client flushes pending analytics events and stops background connections when it goes out of scope. If you are managing the client lifetime manually, call `Dispose()` explicitly: ```csharp client.Dispose(); ``` ## How it works The C# SDK implements `IDisposable` and is designed to live as a long-lived singleton — create one `FeatureflipClient` at startup and share it across your application. In ASP.NET Core, register the instance in DI so all requests share one client with one streaming connection. Creating a client per request would open thousands of redundant SSE connections. On initialization, the client fetches the full flag configuration over HTTPS, then maintains a background `Task` that holds an SSE streaming connection for near-real-time updates. If SSE is unavailable, it falls back to a `Timer`-based polling loop every 30 seconds. All variation methods (`BoolVariation`, `StringVariation`, etc.) read from a `ConcurrentDictionary`, so they are thread-safe and return immediately without `async` overhead — no need to `await` flag evaluations in your controller actions. The SDK targets .NET Standard 2.0 and runs on .NET 8+ and .NET Framework 4.6.2+. Call `Dispose()` during application shutdown (ASP.NET Core handles this automatically for singleton registrations) to close the SSE connection and flush pending analytics events. If the streaming connection drops, the SDK continues returning the last known flag values while it reconnects in the background. ## Troubleshooting **FeatureFlagInitializationException on startup** This means the SDK could not fetch the initial flag configuration. Check that your SDK key is correct and matches the target environment. Verify that your application can reach `eval.featureflip.io` over HTTPS — corporate proxies and firewalls are common blockers. If your app uses `HttpClientFactory`, the SDK creates its own `HttpClient` internally, so proxy settings in `HttpClient` do not apply. **Flags return default values after successful init** Flag keys are case-sensitive — `new-banner` and `New-Banner` are different flags. Verify the key in the dashboard matches your code exactly, and confirm the flag is enabled for the environment your SDK key belongs to. **ObjectDisposedException when evaluating flags** This happens when you call variation methods after the client has been disposed. Make sure the `using` statement or `Dispose()` call happens at the end of your application lifecycle, not prematurely. In ASP.NET Core, register the client as a singleton in the DI container to manage its lifetime. ## Next steps - [C# SDK reference](/docs/sdks/csharp/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/flutter/ # Flutter Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Flutter application. The SDK is a client-side SDK — flags are evaluated server-side and only values are returned. Looking for the full API? The [Flutter SDK reference](/docs/sdks/flutter/) covers every method, every option, and every type — including `FeatureflipBuilder`, lifecycle handling, and offline cache behavior. ## Prerequisites - A Featureflip account with at least one feature flag created - A **client** SDK key from your environment settings - Flags you want to use must have **Client-side visible** enabled - Flutter 3.24+ / Dart 3.5+ ## Install Add the package to your `pubspec.yaml`: ```yaml dependencies: featureflip: ^2.0.0 ``` Then run: ```bash flutter pub get ``` ## Initialize the client ```dart import 'package:featureflip/featureflip.dart'; final config = FeatureflipConfig( clientKey: 'your-client-sdk-key', context: {'user_id': 'user-123'}, ); final client = FeatureflipClient.get('your-client-sdk-key', config: config); await client.initialize(); ``` `initialize()` fetches flag values from the evaluation API and starts SSE streaming for real-time updates. ## Evaluate a flag ```dart final showBanner = client.boolVariation('new-banner', defaultValue: false); if (showBanner) { print('Showing the new banner'); } else { print('Using the default experience'); } ``` The `defaultValue` parameter is returned when the flag is missing or has an unexpected type. Other variation methods are available for different value types: ```dart final color = client.stringVariation('banner-color', defaultValue: 'blue'); final limit = client.numberVariation('rate-limit', defaultValue: 100.0); ``` ## Clean up Close the client when your app is shutting down: ```dart await client.close(); ``` The SDK automatically pauses streaming when the app enters the background and resumes on foreground. ## How it works The Flutter SDK is a **client-side SDK** — flags are evaluated on the Featureflip server, not locally in your app. On initialization, the SDK sends the user context to the evaluation API and receives pre-evaluated flag values. It then opens an SSE (Server-Sent Events) connection that pushes updates in real-time whenever flags change in the dashboard. When the app moves to the background, the SDK pauses the SSE connection to conserve battery and data. It resumes streaming when the app returns to the foreground, fetching any updates that occurred while paused. ## Troubleshooting **initialize() hangs or returns unexpected values** Verify you are using a **client** SDK key (not a server SDK key). Client and server keys are different — using the wrong type results in authentication errors or empty responses. Also confirm that the flags you want to access have **Client-side visible** enabled in the dashboard. **Flag values do not update after changes in the dashboard** The SDK uses SSE streaming for real-time updates. On emulators, SSE connections can be less reliable than on physical devices. If updates seem stuck, try restarting the app or testing on a real device. Also check that your network environment does not block long-lived HTTP connections. **MissingPluginException or build failures** The SDK requires Flutter 3.24+ and Dart 3.5+. Run `flutter --version` to check your version and upgrade if needed. If you see build errors after adding the dependency, run `flutter clean` followed by `flutter pub get` to clear the build cache. ## Next steps - [Flutter SDK reference](/docs/sdks/flutter/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/go/ # Go Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Go application. The SDK requires Go 1.25+. Looking for the full API? The [Go SDK reference](/docs/sdks/go/) covers every option, variation method, and type — including the functional options pattern, `EvaluationDetail`, and the `ForTesting()` helper. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Go 1.25+ ## Install ```bash go get github.com/canopy-labs/featureflip-go/v2 ``` ## Initialize the client ```go package main import ( "fmt" "log" featureflip "github.com/canopy-labs/featureflip-go/v2" ) func main() { client, err := featureflip.Get("your-sdk-key", featureflip.WithBaseURL("https://eval.featureflip.io"), ) if err != nil { log.Fatalf("Failed to initialize: %v", err) } defer client.Close() } ``` `Get` blocks until the initial flag configuration is fetched. It returns an error if the fetch fails or times out (10 seconds by default). If you prefer, set the `FEATUREFLIP_SDK_KEY` environment variable and pass an empty string: ```go client, err := featureflip.Get("") ``` ## Evaluate a flag ```go ctx := featureflip.EvaluationContext{UserID: "user-123"} showBanner := client.BoolVariation("new-banner", ctx, false) if showBanner { fmt.Println("Showing the new banner") } else { fmt.Println("Using the default experience") } ``` The third argument is the default value returned when the flag is not found or evaluation fails. Other variation methods are available for different value types: ```go color := client.StringVariation("banner-color", ctx, "blue") limit := client.Float64Variation("rate-limit", ctx, 100.0) config := client.JSONVariation("ui-config", ctx, map[string]any{}) ``` You can attach custom attributes to the evaluation context for use in targeting rules: ```go ctx := featureflip.EvaluationContext{ UserID: "user-123", Attributes: map[string]any{ "plan": "pro", "country": "US", }, } ``` ## Clean up Use `defer client.Close()` immediately after creating the client, as shown in the initialization example. This flushes pending analytics events and stops background goroutines when your application exits. ## How it works The Go SDK evaluates flags locally in your process — no per-request network calls. `Get` blocks on startup while it fetches the initial flag configuration over HTTPS. Once initialized, the client spawns a background goroutine that maintains an SSE streaming connection to the evaluation API, applying configuration updates as they arrive. If the stream fails to connect five times in a row, the client also polls every 3 seconds while it keeps retrying, and stops polling as soon as the stream delivers again. All variation methods (`BoolVariation`, `StringVariation`, etc.) read from an in-memory store protected by a `sync.RWMutex`, so they are safe to call concurrently from any goroutine without external synchronization. Always pair `Get` with `defer client.Close()` — this stops the background goroutine, flushes pending analytics events, and releases the HTTP connection. Without it, the goroutine and its SSE connection leak until the process exits. If the streaming connection drops, the SDK continues serving the last known configuration. Flag evaluations remain instantaneous throughout — the only thing that pauses is receiving updates. ## Troubleshooting **Get returns an error immediately** The most common cause is an invalid SDK key or an unreachable evaluation API. Verify the key matches your target environment and that your application can reach `eval.featureflip.io` over HTTPS. If you use a custom `WithBaseURL`, make sure it includes the protocol. **Flags return default values even though the dashboard shows them enabled** Flag keys are case-sensitive. Double-check the key string matches the dashboard exactly. Also confirm the flag is enabled in the specific environment that corresponds to your SDK key — a flag enabled in “Production” is not automatically enabled in “Development.” **Import path errors after go get** The module path is `github.com/canopy-labs/featureflip-go/v2`. Ensure your import matches this exactly — the `/v2` suffix is part of the path, not just the version, so dropping it resolves the older v1 line instead. If you see version resolution errors, try running `go mod tidy` to clean up your `go.mod` and `go.sum` files. ## Next steps - [Go SDK reference](/docs/sdks/go/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/java/ # Java Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Java application. The SDK requires Java 11+. Looking for the full API? The [Java SDK reference](/docs/sdks/java/) covers every builder option, variation method, and type — including thread-safety guarantees, `EvaluationContext` builders, and Spring/Micronaut integration patterns. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Java 11+ and Gradle or Maven ## Install ### Gradle build.gradle.kts ```kotlin dependencies { implementation("io.featureflip:featureflip-java:2.3.0") } ``` ### Maven ```xml io.featureflip featureflip-java 2.3.0 ```  io.featureflip featureflip-java 2.3.0"> ## Initialize the client ```java import io.featureflip.client.FeatureflipClient; import io.featureflip.client.EvaluationContext; // Recommended: get a singleton client using the SDK key. try (FeatureflipClient client = FeatureflipClient.get("your-sdk-key")) { EvaluationContext context = EvaluationContext.builder("user-123").build(); boolean showFeature = client.boolVariation("new-feature", context, false); if (showFeature) { // show the new feature } } ``` `FeatureflipClient.get()` returns a handle to a process-wide singleton client. Calling it multiple times with the same SDK key returns handles that share one underlying client — no duplicate streaming connections. Call `waitForInitialization()` to block until the initial flag fetch completes. If you need custom configuration, use the builder (the deduplication still applies): ```java import io.featureflip.client.FeatureflipClient; import io.featureflip.client.FeatureFlagConfig; FeatureFlagConfig config = FeatureFlagConfig.builder() .baseUrl("https://eval.featureflip.io") .streaming(true) .initTimeout(Duration.ofSeconds(30)) .build(); FeatureflipClient client = FeatureflipClient.get("your-sdk-key", config); client.waitForInitialization(); ``` `waitForInitialization()` blocks until the fetch completes or the init timeout expires (10 seconds by default). It throws `FeatureFlagInitializationException` if initialization fails. ## Evaluate a flag ```java import io.featureflip.client.EvaluationContext; EvaluationContext context = EvaluationContext.builder("user-123").build(); boolean showBanner = client.boolVariation("new-banner", context, false); if (showBanner) { System.out.println("Showing the new banner"); } else { System.out.println("Using the default experience"); } ``` The third argument is the default value returned when the flag is not found or evaluation fails. Other variation methods are available for different value types: ```java String color = client.stringVariation("banner-color", context, "blue"); int limit = client.intVariation("rate-limit", context, 100); double ratio = client.doubleVariation("rollout-ratio", context, 0.5); ``` You can attach custom attributes to the evaluation context for use in targeting rules: ```java EvaluationContext context = EvaluationContext.builder("user-123") .set("plan", "pro") .set("country", "US") .build(); ``` ## Clean up The client implements `AutoCloseable`. Use try-with-resources to ensure it flushes pending analytics events and stops background connections: ```java try (FeatureflipClient client = FeatureflipClient.get("your-sdk-key")) { client.waitForInitialization(); boolean result = client.boolVariation("new-banner", EvaluationContext.builder("user-123").build(), false); System.out.println("Flag value: " + result); } ``` If you are managing the client lifetime manually, call `client.close()` explicitly. ## How it works The Java SDK is a **server-side SDK** that evaluates flags locally in your JVM process. On initialization, it fetches the full flag configuration from the Featureflip evaluation API. An SSE streaming connection keeps the configuration up to date in near-real-time — polling every 30 seconds is available as a fallback. Because evaluation happens locally, `boolVariation()` and other variation calls return immediately with no network latency. If the SDK loses its connection to the evaluation API, it continues using the last known configuration. Your application keeps working with the most recent flag values until the connection is restored. ## Troubleshooting **FeatureFlagInitializationException on startup** This means the SDK could not fetch the initial configuration within the timeout (10 seconds by default). Verify your SDK key matches the target environment and that your application can reach `eval.featureflip.io` over HTTPS. In containerized environments, DNS resolution delays are a common cause — check that your `/etc/resolv.conf` is configured correctly. **Flags return default values after successful init** Flag keys are case-sensitive. Verify the key string matches the dashboard exactly. Also confirm the flag is enabled in the environment matching your SDK key — flags are independently toggled per environment. **NoClassDefFoundError or ClassNotFoundException at runtime** This typically means the SDK dependency was not included in your packaged artifact. If you use Gradle’s `implementation` scope, the dependency is included in JAR builds but verify it is on the runtime classpath. For Maven, ensure the scope is `compile` (the default), not `provided`. ## Next steps - [Java SDK reference](/docs/sdks/java/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/javascript/ # JavaScript Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a JavaScript or TypeScript application. The SDK works in both Node.js and browser environments. Looking for the full API? The [JavaScript SDK reference](/docs/sdks/javascript/) covers every option, hook, and method across the package family — `@featureflip/js`, `@featureflip/node`, `@featureflip/browser`, and `@featureflip/react`. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Node.js 20.19.0+ (for Node usage) or a modern browser ## Install ```bash npm install @featureflip/js ``` ## Initialize the client The SDK ships separate platform adapters for Node.js and browsers, both from `@featureflip/js` — the package resolves the right build for your runtime automatically. Import the platform factory that matches your environment. ### Node.js ```ts import { FeatureflipClient, createNodePlatform } from "@featureflip/js"; const client = FeatureflipClient.get( { sdkKey: "your-sdk-key", baseUrl: "https://eval.featureflip.io", }, createNodePlatform() ); await client.waitForInitialization(); ``` ### Browser ```ts import { FeatureflipClient, createBrowserPlatform, } from "@featureflip/js"; const client = FeatureflipClient.get( { sdkKey: "your-sdk-key", baseUrl: "https://eval.featureflip.io", }, createBrowserPlatform() ); await client.waitForInitialization(); ``` `FeatureflipClient.get()` is the only way to obtain a client — the public constructor was removed in v2.0. The factory dedupes by SDK key, so calling `get()` multiple times with the same key returns handles pointing at one shared underlying client. This makes the SDK safe to use from per-request handlers and DI containers without accidentally opening duplicate streaming connections. See [Lifetime](/docs/sdks/javascript/#lifetime) in the reference for details. `waitForInitialization()` fetches your flag configuration from the evaluation API. It rejects if the initial fetch fails or times out. ## Evaluate a flag ```ts const showBanner = client.boolVariation( "new-banner", { user_id: "user-123" }, false ); if (showBanner) { console.log("Showing the new banner"); } else { console.log("Using the default experience"); } ``` The third argument is the default value returned when the flag is not found or evaluation fails. Other variation methods are available for different value types: ```ts const color = client.stringVariation("banner-color", { user_id: "user-123" }, "blue"); const limit = client.numberVariation("rate-limit", { user_id: "user-123" }, 100); const config = client.jsonVariation("ui-config", { user_id: "user-123" }, {}); ``` ## Clean up When your application shuts down, close the client to flush pending analytics events and stop background connections: ```ts await client.close(); ``` ## How it works The JS SDK is a **server-side SDK** that evaluates flags locally in your process. On initialization, it fetches the full flag configuration from the Featureflip evaluation API and opens an SSE (Server-Sent Events) connection that streams updates in real-time whenever flags change in the dashboard. Because evaluation happens locally, `boolVariation()` and other variation calls return immediately with no network latency. **Server-side vs client-side SDKs** Use the JS SDK (this guide) when flags are evaluated in a Node.js backend, a build pipeline, or any server-side environment where you control the runtime. It has access to the full flag configuration and evaluates rules locally. If you are building a **browser application or mobile app** where your end users run the code, use a client-side SDK instead (such as the [Browser SDK](/docs/quickstart/browser/) or [React SDK](/docs/quickstart/react/) ). Client-side SDKs receive pre-evaluated flag values from the server — they never expose your full rule configuration to end users. The SDK provides separate platform adapters for Node.js and browsers. The Node adapter uses `node:http` for requests, while the browser adapter uses the Fetch API. ## Troubleshooting **waitForInitialization() rejects with a network error** Verify your SDK key and that your application can reach the evaluation API (`eval.featureflip.io`) over HTTPS. If you are using the browser platform in development, check that CORS is not blocking the request — the evaluation API allows cross-origin requests, but browser extensions or proxy configurations may interfere. **Flags return default values after successful init** Flag keys are case-sensitive — `new-banner` and `New-Banner` are different flags. Verify the key in the dashboard matches your code exactly. Also confirm the flag is enabled in the environment your SDK key belongs to. **Browser bundle resolves the Node build (or vice versa)** `@featureflip/js` exposes a single entry point whose `exports` map resolves the right build automatically — Node.js via the `node` condition, bundlers via the `browser` condition. There is no `@featureflip/js/browser` subpath; import everything from `@featureflip/js`. If your bundler serves the Node build to the browser (or the reverse), make sure it honors the standard package.json `exports` conditions — older bundlers or TypeScript configurations with `moduleResolution: "node"` may ignore the `exports` map. Set `moduleResolution` to `"bundler"` or `"node16"` in your `tsconfig.json`. ## Next steps - [JavaScript SDK reference](/docs/sdks/javascript/) — full API documentation - [Feature flags in a Next.js app](/blog/feature-flags-in-nextjs/) — server-render the right variant instead of flashing the default - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/nodejs/ # Node.js Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Node.js application. The SDK wraps `@featureflip/js` with Node.js-specific defaults for a simpler setup. Looking for the full API? The [Node.js SDK reference](/docs/sdks/nodejs/) covers every option, evaluation method, and integration pattern — including singleton lifetime, graceful shutdown, and serverless considerations. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Node.js 20.19.0+ ## Install ```bash npm install @featureflip/node ``` ## Initialize the client ```ts import { FeatureflipClient } from "@featureflip/node"; const client = await FeatureflipClient.create({ sdkKey: "your-sdk-key", }); ``` `FeatureflipClient.create()` fetches your flag configuration and resolves once initialization is complete. You can also obtain a client synchronously and wait manually: ```ts const client = FeatureflipClient.get({ sdkKey: "your-sdk-key" }); await client.waitForInitialization(); ``` `FeatureflipClient.get()` is the only way to obtain a client — the public constructor was removed in v2.0. The factory dedupes by SDK key, so calling `get()` multiple times with the same key always returns a handle pointing at one shared underlying client. This makes the SDK safe to use from per-request handlers and DI containers without accidentally opening duplicate streaming connections. See [Lifetime](/docs/sdks/nodejs/#lifetime) in the reference for details. ## Evaluate a flag ```ts const showBanner = client.boolVariation( "new-banner", { user_id: "user-123" }, false, ); if (showBanner) { console.log("Showing the new banner"); } else { console.log("Using the default experience"); } ``` The third argument is the default value returned when the flag is not found or evaluation fails. Other variation methods are available for different value types: ```ts const color = client.stringVariation("banner-color", { user_id: "user-123" }, "blue"); const limit = client.numberVariation("rate-limit", { user_id: "user-123" }, 100); const config = client.jsonVariation("ui-config", { user_id: "user-123" }, {}); ``` ## Clean up When your application shuts down, close the client to flush pending analytics events and stop background connections: ```ts await client.close(); ``` ## How it works The Node.js SDK (`@featureflip/node`) wraps the core `@featureflip/js` evaluation engine with a Node-specific transport layer. It evaluates flags locally in your process — `boolVariation()` and other variation calls are synchronous reads from an in-memory store, so they never block the event loop or add latency to your Express/Fastify/Hono request handlers. `waitForInitialization()` returns a Promise that resolves once the initial flag configuration has been fetched over HTTPS. After that, the SDK maintains an SSE streaming connection on the event loop to receive configuration updates in near-real-time. If SSE is unavailable, it falls back to an interval-based polling loop every 30 seconds. Both mechanisms use Node’s native `http`/`https` modules, so they integrate cleanly with the event loop without spawning threads or child processes. Call `await client.close()` during graceful shutdown (e.g., in a `SIGTERM` handler) to close the SSE connection and flush pending analytics events. If the streaming connection drops mid-operation, the SDK continues serving the last known configuration while it attempts to reconnect in the background. ## Troubleshooting **waitForInitialization() rejects or hangs** The most common cause is an incorrect SDK key or a network issue reaching the evaluation API. Verify your SDK key matches the environment you are targeting (each environment has a unique key). If you are behind a corporate proxy or firewall, ensure outbound HTTPS traffic to `eval.featureflip.io` is allowed. **Flags return default values after successful init** This usually means the flag key does not match exactly. Flag keys are case-sensitive — `new-banner` and `New-Banner` are different flags. Check the dashboard to confirm the key, and make sure the flag is enabled in the environment matching your SDK key. **TypeScript type errors with variation methods** If you are using strict TypeScript, ensure you pass the correct type for the default value. `boolVariation` expects a `boolean` default, `stringVariation` expects a `string`, and so on. Passing `null` or `undefined` as a default is not supported — always provide a typed fallback. ## Next steps - [Node.js SDK reference](/docs/sdks/nodejs/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/php/ # PHP Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a PHP application. The SDK uses PSR-compatible interfaces and works with any framework. Looking for the full API? The [PHP SDK reference](/docs/sdks/php/) covers every option, PSR integration, and method — including PSR-16 cache configuration, custom HTTP clients, and Laravel/Symfony integration. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - PHP 8.2+ - Composer ## Install ```bash composer require featureflip/featureflip-php guzzlehttp/guzzle symfony/cache ``` This installs the SDK along with Guzzle (PSR-18 HTTP client and PSR-17 factories) and Symfony Cache (PSR-16 cache). ## Initialize the client ```php 5, 'connect_timeout' => 2]); $factory = new HttpFactory(); $config = new Config( baseUrl: 'https://eval.featureflip.io', cache: $cache, httpClient: $guzzle, requestFactory: $factory, streamFactory: $factory, ); $client = FeatureflipClient::get('your-sdk-key', $config); ``` 5, 'connect_timeout' => 2]);$factory = new HttpFactory();$config = new Config( baseUrl: 'https://eval.featureflip.io', cache: $cache, httpClient: $guzzle, requestFactory: $factory, streamFactory: $factory,);$client = FeatureflipClient::get('your-sdk-key', $config);"> The factory fetches your flag configuration when it is due a refresh, and registers a shutdown function to flush events automatically. The same check runs when you evaluate a flag, so a client held across many requests — Laravel Octane, RoadRunner, FrankenPHP, Swoole — keeps picking up changes rather than freezing at whatever it booted with. The SDK key can also come from the `FEATUREFLIP_SDK_KEY` environment variable: pass an empty string and it is read from there. ## Evaluate a flag ```php $showBanner = $client->boolVariation('new-banner', ['user_id' => 'user-123'], false); if ($showBanner) { echo "Showing the new banner"; } else { echo "Using the default experience"; } ``` boolVariation('new-banner', ['user_id' => 'user-123'], false);if ($showBanner) { echo "Showing the new banner";} else { echo "Using the default experience";}"> Each variation method is typed to its return value. The default argument is returned when the flag is not found or evaluation fails. ```php $color = $client->stringVariation('banner-color', ['user_id' => 'user-123'], 'blue'); $limit = $client->numberVariation('rate-limit', ['user_id' => 'user-123'], 100); ``` stringVariation('banner-color', ['user_id' => 'user-123'], 'blue');$limit = $client->numberVariation('rate-limit', ['user_id' => 'user-123'], 100);"> ## Clean up The SDK registers a shutdown function that flushes pending analytics events when the PHP process exits. You can also flush or close manually: ```php $client->flush(); // Send buffered events immediately $client->close(); // Flush events and finish the request (PHP-FPM) ``` flush(); // Send buffered events immediately$client->close(); // Flush events and finish the request (PHP-FPM)"> For long-running processes, call `$client->close()` when your application shuts down. ## How it works The PHP SDK is a **server-side SDK** that evaluates flags locally in your PHP process. It fetches the full flag configuration from the Featureflip evaluation API and stores it in the PSR-16 cache you provide. Requests made before `pollInterval` has elapsed reuse that configuration without any API call. Because evaluation happens locally, `boolVariation()` and the other variation calls return immediately. Unlike the other server SDKs, PHP has no background thread to poll on: it re-fetches when the interval has elapsed and something asks for a flag. Under PHP-FPM that is naturally once per interval across your workers; under a persistent runtime (Octane, RoadRunner, FrankenPHP, Swoole) the same check runs on the evaluation path, and you can call `$client->refresh()` from a tick hook to move the fetch between requests instead of inside one. The stored configuration is **kept**, not expired on a timer. Only a successful fetch replaces it, so an evaluation API outage leaves your flags serving their last known good values instead of the defaults you pass at each call site. ## Troubleshooting **Flags are stale or do not update after changes in the dashboard** Flag changes take effect once `pollInterval` has elapsed — 30 seconds by default. Lower it for faster propagation: ```php $config = new Config(pollInterval: 5, /* ... */); ``` Note this is a *refresh* interval, not a cache lifetime. A configuration that has been fetched successfully is kept, so if the Evaluation API becomes unreachable your flags keep serving their last known good values rather than falling back to the defaults you pass at each call site. **Every flag returns the default I passed** The SDK does not throw when it cannot load a configuration — it reports the failure and serves your defaults, so a bad SDK key or an unreachable Evaluation API looks like “every flag is off” rather than an exception. Two things to check: ```php if (!$client->isInitialized()) { // Nothing was ever loaded — the SDK is serving your defaults. } ``` isInitialized()) { // Nothing was ever loaded — the SDK is serving your defaults.}"> and the log. The SDK writes to PHP’s error log by default, or to any PSR-3 logger you pass as `logger:`. A rejected key looks like: ```plaintext [featureflip] flag configuration fetch failed: HTTP 401 from /v1/sdk/flags - no cached configuration is available, so flags will serve their caller defaults ``` Verify the key belongs to the environment you expect, and that `baseUrl` includes the protocol (`https://`). **InvalidArgumentException from the cache layer** PSR-16 cache keys have reserved characters (`{}()/\@:`). The SDK handles this internally, but if you see this error, ensure you are using a PSR-16 compliant cache implementation. Symfony Cache and Laravel’s cache are both compatible. ## Next steps - [PHP SDK reference](/docs/sdks/php/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/python/ # Python Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Python application. The SDK is synchronous and thread-safe. Looking for the full API? The [Python SDK reference](/docs/sdks/python/) covers every option, evaluation method, and integration pattern — including the context manager protocol, Django/Flask/FastAPI usage, and event tracking. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Python 3.10+ ## Install ```bash pip install featureflip ``` ## Initialize the client ```python from featureflip import FeatureflipClient, Config config = Config(base_url="https://eval.featureflip.io") client = FeatureflipClient(sdk_key="your-sdk-key", config=config) ``` The client fetches your flag configuration on startup and blocks until initialization completes. If you prefer, set the `FEATUREFLIP_SDK_KEY` environment variable and omit the `sdk_key` parameter: ```python client = FeatureflipClient(config=config) ``` ## Evaluate a flag ```python show_banner = client.variation("new-banner", {"user_id": "user-123"}, default=False) if show_banner: print("Showing the new banner") else: print("Using the default experience") ``` The `variation()` method works for all flag types. The `default` argument is returned when the flag is not found or evaluation fails. Lifetime Multiple `FeatureflipClient(sdk_key="...")` calls with the same SDK key share one underlying client. Closing one does not shut down the shared connection while others are still alive. See the [Lifetime](/docs/sdks/python/#lifetime) section of the SDK reference for details. ```python color = client.variation("banner-color", {"user_id": "user-123"}, default="blue") limit = client.variation("rate-limit", {"user_id": "user-123"}, default=100) ``` ## Clean up Close the client when your application shuts down to flush pending analytics events and stop background connections. The recommended approach is a context manager: ```python from featureflip import FeatureflipClient, Config config = Config(base_url="https://eval.featureflip.io") with FeatureflipClient(sdk_key="your-sdk-key", config=config) as client: result = client.variation("new-banner", {"user_id": "user-123"}, default=False) print(f"Flag value: {result}") ``` If you are not using a context manager, call `client.close()` explicitly. ## How it works The Python SDK evaluates flags locally in your process — `variation()` is a synchronous call that reads from an in-memory dictionary, so it works naturally in Django views, Flask routes, or any synchronous Python code without blocking the request. On initialization, the client fetches the full flag configuration over HTTPS and then opens a background thread to maintain an SSE streaming connection. This thread receives configuration updates in near-real-time; if SSE is unavailable, it falls back to polling every 30 seconds. The in-memory flag store is protected by a `threading.Lock`, making it safe to call `variation()` from multiple threads in a WSGI server like Gunicorn with threaded workers. The GIL ensures the lock operations themselves are fast, so contention is negligible in practice. Use the context manager pattern (`with FeatureflipClient(...) as client`) to ensure the background thread and SSE connection are cleaned up on shutdown. If the connection drops, the SDK continues returning the last known flag values until it reconnects. ## Troubleshooting **Client initialization is slow or times out** Verify your SDK key and that your application can reach `eval.featureflip.io` over HTTPS. If you are in a restricted network environment, check that outbound HTTPS traffic is not blocked by a firewall or proxy. The `base_url` parameter must include the protocol (`https://`). **variation() returns the default value for every flag** Check that the flag key matches exactly (keys are case-sensitive) and that the flag is enabled in the environment matching your SDK key. Also verify you are passing the context as a dictionary with a `user_id` key — without it, targeting rules that depend on user identity will not match. **ImportError or ModuleNotFoundError after install** Make sure you installed `featureflip` (the PyPI package name), not a similarly named package. If you are using a virtual environment, confirm it is activated before running your script. Run `pip show featureflip` to verify the package is installed in the correct environment. ## Next steps - [Python SDK reference](/docs/sdks/python/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/ruby/ # Ruby Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Ruby application. The SDK is thread-safe and has zero runtime dependencies. Looking for the full API? The [Ruby SDK reference](/docs/sdks/ruby/) covers every option, variation method, and type — including Rails initializer patterns, Sidekiq usage, and the testing API. ## Prerequisites - A Featureflip account with at least one feature flag created - An SDK key from your environment settings - Ruby 3.2+ ## Install Add to your Gemfile: ```ruby gem "featureflip" ``` Then run: ```bash bundle install ``` ## Initialize the client ```ruby require "featureflip" config = Featureflip::Config.new(base_url: "https://eval.featureflip.io") client = Featureflip::Client.new(sdk_key: "your-sdk-key", config: config) ``` The client fetches your flag configuration on startup and blocks until initialization completes. If you prefer, set the `FEATUREFLIP_SDK_KEY` environment variable and omit the `sdk_key` parameter: ```ruby client = Featureflip::Client.new(config: config) ``` ## Evaluate a flag ```ruby show_banner = client.bool_variation("new-banner", { user_id: "user-123" }, false) if show_banner puts "Showing the new banner" else puts "Using the default experience" end ``` The SDK provides typed methods for each flag type. The `default_value` argument is returned when the flag is not found or evaluation fails. ```ruby color = client.string_variation("banner-color", { user_id: "user-123" }, "blue") limit = client.number_variation("rate-limit", { user_id: "user-123" }, 100) ``` ## Clean up Close the client when your application shuts down to flush pending analytics events and stop background connections: ```ruby client.close ``` For Rails applications, use the singleton API with an initializer: config/initializers/featureflip.rb ```ruby Featureflip.configure do |c| c.sdk_key = ENV["FEATUREFLIP_SDK_KEY"] c.streaming = true end # In your application code enabled = Featureflip.bool_variation("new-banner", { user_id: "user-123" }, false) # On shutdown Featureflip.close ``` ## How it works The Ruby SDK uses a singleton pattern — `Featureflip.configure` initializes one shared client for your entire process. This matches how most Ruby web frameworks operate: Puma and Unicorn fork worker processes, and each worker holds its own SDK instance with its own flag store and streaming connection. On initialization, the client fetches the full flag configuration over HTTPS, then spawns a background thread that maintains an SSE streaming connection for near-real-time updates. Polling every 30 seconds is available as a fallback. The internal flag store is protected by a `Mutex`, so `bool_variation` and other variation calls are thread-safe — important for Puma’s multi-threaded workers where concurrent requests share the same process. Call `Featureflip.close` during shutdown (e.g., in an `at_exit` hook or Rails initializer teardown) to stop the background thread and flush pending analytics events. If the streaming connection drops, the SDK keeps returning the last known flag values until it reconnects. ## Troubleshooting **Client initialization hangs or times out** Verify your SDK key matches the target environment and that your application can reach `eval.featureflip.io` over HTTPS. If you use Bundler behind a corporate proxy, the SDK’s HTTP client also needs proxy access — check your `HTTP_PROXY` / `HTTPS_PROXY` environment variables. **Flags return default values after successful init** Flag keys are case-sensitive. Verify the key matches the dashboard exactly, and confirm the flag is enabled in the environment matching your SDK key. **LoadError when requiring featureflip** Make sure the gem is installed: run `gem list featureflip` to check. If you are using Bundler, ensure `gem "featureflip"` is in your Gemfile and you have run `bundle install`. Run your script with `bundle exec` to use the Bundler-managed gem. ## Next steps - [Ruby SDK reference](/docs/sdks/ruby/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/quickstart/swift/ # Swift Feature Flags Quickstart This guide gets you from zero to evaluating a [feature flag](/docs/concepts/feature-flags/) in a Swift application. The SDK is a client-side SDK — flags are evaluated server-side and only values are returned. Looking for the full API? The [Swift SDK reference](/docs/sdks/swift/) covers every method, every option, and every type — including `identify()`, lifecycle behavior, and the offline disk cache. ## Prerequisites - A Featureflip account with at least one feature flag created - A **client** SDK key from your environment settings - Flags you want to use must have **Client-side visible** enabled - Swift 5.9+, targeting iOS 15+ / macOS 13+ / tvOS 15+ / watchOS 8+ ## Install Add the package to your project via Swift Package Manager. In Xcode: **File → Add Package Dependencies**, then enter the repository URL. Or in `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/canopy-labs/featureflip-swift", from: "2.0.0") ] ``` ## Initialize the client ```swift import Featureflip let config = FeatureflipConfig( clientKey: "your-client-sdk-key", context: ["user_id": "user-123"] ) let client = FeatureflipClient(config: config) await client.initialize() ``` `initialize()` fetches flag values from the evaluation API and starts SSE streaming for real-time updates. Cached values from disk are used while the network request is in flight. ## Evaluate a flag ```swift let showBanner = client.boolVariation("new-banner", default: false) if showBanner { print("Showing the new banner") } else { print("Using the default experience") } ``` The `default` parameter is returned when the flag is missing or has an unexpected type. Other variation methods are available for different value types: ```swift let color = client.stringVariation("banner-color", default: "blue") let limit = client.numberVariation("rate-limit", default: 100.0) ``` ## Clean up Close the client when your app is shutting down: ```swift await client.close() ``` The SDK automatically pauses streaming when the app enters the background and resumes on foreground. ## How it works The Swift SDK is a **client-side SDK** — flags are evaluated on the Featureflip server, not locally in your app. On initialization, the SDK sends the user context to the evaluation API and receives pre-evaluated flag values. It then opens an SSE (Server-Sent Events) connection that pushes updates in real-time whenever flags change in the dashboard. Flag values are cached to disk, so your app has working values immediately on launch while the SDK fetches fresh data in the background. When the app moves to the background, the SDK pauses the SSE connection to conserve battery and resumes it when the app returns to the foreground. ## Troubleshooting **initialize() hangs or returns unexpected values** Verify you are using a **client** SDK key (not a server SDK key). Client and server keys are different — using the wrong type results in authentication errors or empty responses. Also confirm that the flags you want to access have **Client-side visible** enabled in the dashboard. **Flag values do not update in real-time** The SDK uses SSE streaming for updates. If your app is behind a network that blocks long-lived connections (some corporate Wi-Fi or VPN configurations), updates may be delayed until the next polling cycle. Check your device’s network connectivity if updates seem stuck. **Xcode build errors after adding the package** Ensure your deployment target meets the minimum: iOS 15, macOS 13, tvOS 15, or watchOS 8. If Xcode shows “Missing package product” errors, try resetting the package cache: **File → Packages → Reset Package Caches**. Also verify you are using Swift 5.9+ in your project settings. ## Next steps - [Swift SDK reference](/docs/sdks/swift/) — full API documentation - [Targeting rules](/docs/concepts/targeting/) — deliver different values to different users - [Rollout strategies](/docs/concepts/rollout-strategies/) — gradually release features with percentage rollouts - [How to Create Feature Flags](/docs/guides/creating-flags/) — create and manage flags from the dashboard - [Environments](/docs/concepts/environments/) — manage separate configurations for dev, staging, and production --- ## Source: https://featureflip.io/docs/sdks/android/ # Android SDK Reference **TL;DR:** Add `io.featureflip:featureflip-android`, call `client.boolVariation("flag-key", false)`, and you’re evaluating flags from any Android app — Kotlin-first, lifecycle-aware. The Featureflip Android SDK is a Kotlin-first, client-side library for evaluating [feature flags](/docs/concepts/feature-flags/) in Android applications. Like the other client SDKs, it receives *pre-evaluated* flag values from the Featureflip Evaluation API rather than running an evaluator on-device — your targeting rules never ship inside your APK or AAB. The SDK is published to Maven Central as `io.featureflip:featureflip-android` and works with any Android project on minSdk 24+ (Android 7.0). The SDK is built around `kotlinx.coroutines` end-to-end. Initialization is a `suspend` function so you can `await` it from a `lifecycleScope` or `viewModelScope`, and flag updates are exposed as `StateFlow`s that integrate cleanly with Jetpack Compose, View Binding, and Data Binding. A `composable` extension function lets you read a flag’s current value from a Compose function and re-render automatically when [percentage rollouts](/docs/concepts/rollout-strategies/) shift the assignment. The streaming connection is automatically paused when your activity goes to the background and resumed when it returns to the foreground, so you don’t burn battery on idle devices. For offline starts, the SDK transparently caches the latest snapshot to disk via `DataStore`. If the user opens your app in airplane mode or on a flaky network, [targeting rules](/docs/concepts/targeting/) still resolve to the values from the last successful sync, and the SDK quietly retries the connection in the background. The companion [Java SDK](/docs/sdks/java/) shares the underlying evaluation engine, so flag values for the same context are guaranteed identical across your Android client and your JVM backend services. New to Featureflip? Start with the [Android quickstart](/docs/quickstart/android/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-android) ## Installation Add the dependency to your `build.gradle.kts`: ```kotlin dependencies { implementation("io.featureflip:featureflip-android:2.3.0") } ``` ## Configuration The `FeatureflipConfig` data class accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | clientKey | String | required | Client SDK key from your project settings | | baseUrl | String | "https://eval.featureflip.io" | Evaluation API base URL | | context | Map | emptyMap() | Initial evaluation context (user attributes) | | streaming | Boolean | true | Enable SSE for real-time flag updates | | pollIntervalMs | Long | 30_000 | Polling interval in milliseconds (used when streaming is disabled) | | flushIntervalMs | Long | 30_000 | Interval in milliseconds between event flush batches | | flushBatchSize | Int | 100 | Maximum number of events per flush batch | | initTimeoutMs | Long | 10_000 | Maximum time in milliseconds to wait for initial flag fetch | ### Example configuration ```kotlin val config = FeatureflipConfig( clientKey = "your-client-sdk-key", context = mapOf("user_id" to "u-123"), streaming = true, pollIntervalMs = 60_000, initTimeoutMs = 15_000, ) ``` ## Initialization Obtain a client via the static factory `FeatureflipClient.get(config)`. There is no public constructor — the factory dedupes by client key, so every call with the same key returns a handle pointing at one shared underlying client. See Lifetime for the full contract. ```kotlin import dev.featureflip.android.FeatureflipClient import dev.featureflip.android.FeatureflipConfig val config = FeatureflipConfig(clientKey = "your-client-sdk-key") val client = FeatureflipClient.get(config) client.initialize() ``` `initialize()` loads any cached flags from disk, makes a synchronous fetch for fresh flag values from the API, then starts SSE streaming (or polling) and the event processor. The SDK is usable with cached values even if the network request fails. Call `initialize()` from a background thread to avoid blocking the main thread. Access from anywhere: ```kotlin val enabled = FeatureflipClient.get(config).boolVariation("my-feature", false) ``` Calling `get()` again with the same `clientKey` returns a new handle pointing at the cached shared core — no new HTTP request, no new SSE connection. Only the first call does the real work. ## Lifetime The Android SDK is **singleton-by-construction**. You cannot `new` a client directly, and every `FeatureflipClient.get(config)` call with the same `clientKey` returns a handle pointing at one shared background client. This is intentional — a Featureflip client maintains a long-lived SSE streaming connection, a periodic event flush loop, a disk cache, and a lifecycle observer. Opening more than one per client key per process wastes battery, bandwidth, and causes flag updates to fan out unpredictably. The factory **refcounts** the shared core using `AtomicInteger`: - The first `get()` for a given client key constructs the shared core and returns a handle with refcount `1`. - Subsequent `get()` calls with the same key return a new handle and atomically increment the refcount. The `config` argument on repeat calls is ignored; if the baseUrl / streaming / intervals differ meaningfully, a warning is logged. - `handle.close()` decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: stops streaming/polling, removes the lifecycle observer, flushes pending events, and removes itself from the factory cache. - Double-closing the same handle is a no-op — the handle tracks its own disposed state via `AtomicBoolean` and will not double-decrement the refcount. - A different client key always constructs an independent shared core. This makes the SDK safe under any lifetime pattern: - **Application-level singleton** (the recommended pattern): call `get()` once in your `Application.onCreate()`, inject the handle everywhere, `close()` it in `onTerminate()` (if you care about clean shutdown in tests). - **Per-Activity or per-ViewModel access**: calling `get()` in each Activity’s `onCreate()` or each ViewModel constructor is harmless. Each call returns a fresh handle on the same underlying shared core. Close the handle in the corresponding `onDestroy()` / ViewModel `onCleared()` to decrement the refcount; the core stays alive as long as at least one handle references it. - **DI container misregistration**: Hilt/Koin/Dagger scoped or transient bindings become harmless. Each resolution allocates a handle, not a new HTTP client or SSE connection. Use `FeatureflipClient.forTesting(overrides)` to obtain a standalone test client that is **not** registered in the factory cache — each call returns an independent instance with no background connections. ### isInitialized ```kotlin client.isInitialized: Boolean ``` A read-only property that returns `true` once `initialize()` has completed (regardless of whether the network fetch succeeded — cached flags may be in use), and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags All variation methods are synchronous and return immediately from an in-memory snapshot. They accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type. Since this is a client-side SDK, evaluation context is set at initialization or via `identify()` — it is not passed per evaluation call. ### boolVariation(key, defaultValue) ```kotlin val enabled = client.boolVariation("dark-mode", false) ``` ### stringVariation(key, defaultValue) ```kotlin val color = client.stringVariation("button-color", "blue") ``` ### numberVariation(key, defaultValue) ```kotlin val limit = client.numberVariation("rate-limit", 100.0) ``` Returns a `Double`. ### jsonVariation(key, defaultValue) ```kotlin val config = client.jsonVariation("ui-config", null) ``` Returns `Any?` — the raw deserialized value. ## Identifying Users Call `identify()` to re-evaluate all flags for a new user context — typically after login. This makes a network request to the evaluation API with the new context and updates all cached flag values. ```kotlin client.identify(mapOf("user_id" to "u-123", "plan" to "pro")) ``` The streaming or polling data source is also updated with the new context. ## Event Tracking ### track(eventName, metadata?) Enqueues a custom analytics event. Events are batched and flushed automatically. ```kotlin client.track("purchase-completed", mapOf( "plan" to "pro", "amount" to 29.99, )) ``` Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### flush() Starts sending all buffered events immediately and returns. Sending them is a network round-trip, so it runs on a background dispatcher. Calling this from the main thread is safe. ```kotlin client.flush() ``` ### flushAndAwait() Suspending version of `flush()`. Returns once the send has been attempted, so use it when you need the events actually delivered before moving on. ```kotlin client.flushAndAwait() ``` ## Cleanup ### close() Decrements the refcount on the shared core. When the last handle for a given client key is closed, the shared core stops streaming/polling, removes the lifecycle observer, flushes remaining events, and removes itself from the factory cache. Double-close on the same handle is a no-op. Safe to call from the main thread. The final flush runs on a background dispatcher, so this returns before it completes. ```kotlin client.close() ``` ### closeAndAwait() Suspending version of `close()`. Returns once the final flush has been attempted. Reach for it from a scope that is still alive and where the buffered events matter, such as a sign-out handler or an application-scoped coroutine. Do not reach for it in `ViewModel.onCleared()`. `viewModelScope` is already cancelled by then, so `viewModelScope.launch { }` there never runs its body at all, and `onCleared()` is not a suspend context either. Call plain `close()` from `onCleared()`. ```kotlin client.closeAndAwait() ``` ### Lifecycle management On Android with `androidx.lifecycle:lifecycle-process` on the classpath, the SDK automatically observes app lifecycle events: - **Background**: Stops streaming/polling and flushes events - **Foreground**: Resumes streaming/polling No manual intervention is required. ## Testing ### FeatureflipClient.forTesting(overrides) Creates a test client with static flag overrides. No network calls are made and no background tasks are started. The client is immediately initialized. ```kotlin val client = FeatureflipClient.forTesting(mapOf( "dark-mode" to true, "pricing-tier" to "enterprise", "rate-limit" to 500, )) client.boolVariation("dark-mode", false) // true client.stringVariation("pricing-tier", "free") // "enterprise" client.boolVariation("unknown", false) // false (default) ``` **Signature:** ```kotlin fun forTesting(overrides: Map): FeatureflipClient ``` ): FeatureflipClient"> Supported override value types: `Boolean`, `String`, `Int`, `Double`. ## Types ### FlagValue The internal representation of a flag returned by the server: | Property | Type | Description | | --- | --- | --- | | value | Any? | The evaluated flag value | | variation | String | The key of the matched variation | | reason | String | Why this value was returned | ### Flag reasons | Value | Description | | --- | --- | | "Fallthrough" | No rules matched; the fallthrough variation was served | | "RuleMatch" | A targeting rule matched the context | | "FlagDisabled" | The flag is disabled; the off variation was served | | "FlagNotFound" | The flag key does not exist | | "Error" | An error occurred during evaluation | | "TEST" | Returned by test clients | ## Requirements | Platform | Minimum Version | | --- | --- | | Android API | 21 | | Kotlin | 1.9+ | | Java | 17+ | ## See also - [Android Quickstart](/docs/quickstart/android/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/browser/ # Browser SDK Reference **TL;DR:** Install `@featureflip/browser`, call `client.boolVariation('flag-key', false)`, and you’re evaluating flags in the browser — context comes from `identify()`. The `@featureflip/browser` package is the framework-agnostic Featureflip client for browser applications — vanilla JS, Vue, Svelte, Solid, Lit, Alpine, jQuery, or anything else that runs in a `` tag. (For React specifically, use [`@featureflip/react`](/docs/sdks/react/) , which adds a provider, hooks, and StrictMode-safe lifecycle handling on top of this package.) Unlike the server SDKs, the Browser SDK is a *client-side* SDK: it receives pre-evaluated [feature flag](/docs/concepts/feature-flags/) values from the Evaluation API rather than running an evaluator locally. This keeps the bundle small and prevents shipping your targeting rules to every visitor. Real-time updates arrive via the browser’s native `EventSource`, which means authentication is passed through the URL query string (browsers prohibit setting custom `Authorization` headers on `EventSource`). The SDK handles the auth flow automatically; you just provide your client SDK key. After the initial snapshot, the server streams *delta* updates — only the flags that changed — and the SDK merges them into the local cache rather than replacing it. Flags removed via [targeting rules](/docs/concepts/targeting/) or deleted in the dashboard are dropped from the snapshot in place. The Browser SDK is **singleton-by-construction**: there is no public constructor, only the static factory `FeatureflipClient.get(config)`. Every call with the same client key returns a handle pointing at one shared underlying client. This makes framework bindings, React StrictMode double-mounts, and per-component construction all harmless — they all resolve to one SSE connection and one flag store per key. See the Lifetime section for the full contract. `identify()` updates the active context (for example, after a user logs in) by closing the previous SSE stream and reopening with the new context. This is the right SDK for SPAs where you need [percentage rollouts](/docs/concepts/rollout-strategies/) and targeting to react to login state in real time. New to Featureflip? Start with the [JavaScript quickstart](/docs/quickstart/javascript/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-browser) ## Requirements - Any modern browser with `fetch` and `EventSource` support - Node.js 20.19.0+ (for build tooling) ## Installation ```bash npm install @featureflip/browser ``` ## Quick Start ```ts import { FeatureflipClient } from '@featureflip/browser'; const client = FeatureflipClient.get({ clientKey: 'your-client-sdk-key', }); await client.initialize(); const showBanner = client.boolVariation('show-banner', false); ``` `FeatureflipClient.get()` is the only way to obtain a client — the public constructor was removed in v2.0. See Lifetime for details. ## Configuration ```ts FeatureflipClient.get(config: FeatureflipClientConfig) ``` | Option | Type | Default | Description | | --- | --- | --- | --- | | clientKey | string | (required) | Client SDK key from your project settings | | baseUrl | string | https://eval.featureflip.io | Evaluation API base URL | | context | Record | {} | Initial evaluation context (user attributes) | | streaming | boolean | true | Enable SSE streaming for real-time updates | | initTimeout | number | 10000 | Timeout in ms for the initial evaluate request | ## Initialization Call `initialize()` before reading any flag values. This fetches all client-visible flags from the server and opens an SSE connection if streaming is enabled. ```ts const client = FeatureflipClient.get({ clientKey: 'your-client-sdk-key', }); await client.initialize(); ``` ## Lifetime The Browser SDK is singleton-by-construction: you cannot `new` a client directly, and every `FeatureflipClient.get(config)` call with the same `clientKey` returns a handle pointing at one shared background client. This is intentional — a browser client maintains a long-lived SSE streaming connection and an in-memory flag store. Opening more than one per `clientKey` per page wastes bandwidth and causes flag updates to fan out unpredictably. The factory **refcounts** the shared core: - The first `get()` for a given client key constructs the shared core and returns a handle with refcount `1`. - Subsequent `get()` calls with the same key return a new handle and increment the refcount. The `config` argument on repeat calls is ignored; if the resolved config differs meaningfully from the first call, a warning is logged. - `handle.close()` decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: closes the SSE stream and removes itself from the factory cache. - Double-closing the same handle is a no-op — the handle tracks its own disposed state and will not double-decrement. - A different client key always constructs an independent shared core. This makes the SDK safe under any lifetime pattern: - **React StrictMode**: mount → cleanup → remount cycles return new handles over the same core. The cleanup’s `close()` drives the refcount from 2 → 1 (not 0), so the SSE connection stays alive across the simulated remount. This is exactly what `@featureflip/react`’s `FeatureflipProvider` relies on. - **Per-component construction**: framework bindings can call `get()` inside component render without worrying about duplicating SSE connections. Each call allocates a fresh handle, not a new network connection. - **Micro-frontend architectures**: independent React/Vue/Svelte islands sharing a client key on the same page share one underlying connection. Use `FeatureflipClient.forTesting(flags)` to obtain a standalone test client that is **not** registered in the factory cache — each call returns an independent instance with no background connections. ## Evaluating Flags All variation methods accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type. ### boolVariation(key, defaultValue) ```ts const enabled = client.boolVariation('dark-mode', false); ``` ### stringVariation(key, defaultValue) ```ts const color = client.stringVariation('button-color', 'blue'); ``` ### numberVariation(key, defaultValue) ```ts const limit = client.numberVariation('rate-limit', 100); ``` ### jsonVariation(key, defaultValue) ```ts const config = client.jsonVariation<{ sidebar: boolean }>('ui-config', { sidebar: true }); ``` ('ui-config', { sidebar: true });"> ## Identifying Users Call `identify()` to re-evaluate all flags with a new user context — typically after login or when user attributes change. Emits `change` events for any flags whose values changed. ```ts await client.identify({ user_id: '123', plan: 'pro' }); ``` ## Events ### on(event, handler) Subscribe to lifecycle events. | Event | Description | | --- | --- | | 'ready' | Fired after initialize() completes | | 'change' | Fired when flag values change (receives a FlagChanges object) | | 'error' | Fired on streaming or network errors | ```ts client.on('change', (changes) => { console.log('Flags changed:', changes); }); ``` { console.log('Flags changed:', changes);});"> ### off(event, handler) Unsubscribe from events. ## Cleanup ### close() Decrements the refcount on the shared core. When the last handle for a given client key is closed, the shared core closes the SSE connection and removes itself from the factory cache. Double-closing the same handle is a no-op. A closed handle serves the caller’s default from every accessor — it does not keep returning the last-known values, which could never refresh once the connection is gone. ```ts client.close(); ``` ## Testing Use `FeatureflipClient.forTesting()` to create a client with predetermined flag values — no network calls, no initialization needed. ```ts const client = FeatureflipClient.forTesting({ 'show-banner': true, 'button-color': 'blue', }); client.boolVariation('show-banner', false); // true client.stringVariation('button-color', 'red'); // 'blue' ``` ## See also - [JavaScript Quickstart](/docs/quickstart/javascript/) — get started in under 5 minutes - [React SDK](/docs/sdks/react/) — React-specific bindings with provider and hooks - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/csharp/ # C# SDK Reference **TL;DR:** Install `Featureflip.Client`, call `client.BoolVariation("flag-key", context, false)`, and you’re evaluating flags — synchronous, no `await` in your hot path. The `Featureflip.Client` NuGet package provides a thread-safe client for evaluating [feature flags](/docs/concepts/feature-flags/) in .NET applications. It multi-targets .NET 8.0, .NET 10.0, and .NET Standard 2.0, so the same package works across modern .NET, .NET Framework 4.6.1+, and any other runtime that supports .NET Standard 2.0. Real-time updates arrive over SSE streaming, with a polling fallback when streaming is unavailable. The SDK is designed as a process-wide singleton. `FeatureflipClient.Get(sdkKey)` returns handles backed by a shared, refcounted client, so you can resolve it from anywhere — including misregistered DI scopes — without opening duplicate streaming connections. The companion `Featureflip.Client.Extensions.DependencyInjection` package wires this up correctly for ASP.NET Core with `services.AddFeatureflip(...)`, and the same singleton survives the entire application lifetime. Variation methods (`BoolVariation`, `StringVariation`, `IntVariation`, `DoubleVariation`, `JsonVariation`) read from a `ConcurrentDictionary` and return synchronously — no `await` required in your controller actions or hot paths. Beyond `Microsoft.Extensions.Logging.Abstractions` and `System.Text.Json`, the SDK has no direct dependencies, keeping the deployed footprint small. New to Featureflip? Start with the [C# quickstart](/docs/quickstart/csharp/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-dotnet) ## Lifetime The Featureflip C# SDK is designed to be used as a singleton across your application. Obtain the client via the static factory: ```csharp var client = FeatureflipClient.Get("your-sdk-key"); ``` Calling `Get()` multiple times with the same SDK key returns handles that share a single underlying client — the SDK maintains a process-wide cache keyed by SDK key. You cannot accidentally open duplicate streaming connections by constructing multiple clients. If you’re using `Microsoft.Extensions.DependencyInjection`, the `AddFeatureflip` extension method registers the client correctly as a singleton: ```csharp services.AddFeatureflip("your-sdk-key"); ``` Even if you hand-roll a scoped or transient DI registration, the factory’s deduplication ensures all resolves share one underlying client — the misregistration is neutralized, not catastrophic. ### Disposal `Dispose()` is refcounted: when multiple handles share one cached client, disposing one handle does not shut down the shared background tasks. The real shutdown runs only when the last handle is disposed. In a typical application this means the client lives for the entire process lifetime and is cleaned up automatically at shutdown. ## Requirements The SDK multi-targets the following frameworks: | Target | Minimum Version | | --- | --- | | .NET | 8.0 | | .NET | 10.0 | | .NET Standard | 2.0 (.NET Framework 4.6.1+, .NET Core 2.0+) | ## Installation ```bash dotnet add package Featureflip.Client ``` For ASP.NET Core dependency injection support, also install the extensions package: ```bash dotnet add package Featureflip.Client.Extensions.DependencyInjection ``` ## Configuration The `FeatureFlagOptions` class accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | BaseUrl | string | "https://eval.featureflip.io" | Base URL of the Evaluation API | | ConnectTimeout | TimeSpan | 5s | Timeout for establishing connections | | ReadTimeout | TimeSpan | 10s | Timeout for reading responses | | Streaming | bool | true | Enable SSE for real-time flag updates | | PollInterval | TimeSpan | 30s | Polling interval (used when streaming is disabled) | | FlushInterval | TimeSpan | 30s | Interval between event flush batches | | FlushBatchSize | int | 100 | Maximum number of events per flush batch | | InitTimeout | TimeSpan | 10s | Maximum time to wait for initial flag fetch | | WaitForInitialization | bool | false | Block the constructor until flags are loaded | | StartOffline | bool | false | Start the client even if initial flag loading fails | ### Example configuration ```csharp var options = new FeatureFlagOptions { BaseUrl = "https://eval.featureflip.io", Streaming = true, PollInterval = TimeSpan.FromSeconds(60), FlushInterval = TimeSpan.FromSeconds(15), InitTimeout = TimeSpan.FromSeconds(30), WaitForInitialization = true, }; ``` ## Initialization Create a client by providing an SDK key and optional options: ```csharp using var client = FeatureflipClient.Get( "sdk-dev-abc123", new FeatureFlagOptions { WaitForInitialization = true } ); ``` ### SDK key resolution If `sdkKey` is `null`, the client falls back to the `FEATUREFLIP_SDK_KEY` environment variable. A `FeatureFlagInitializationException` is thrown if neither is available. ### Initialization behavior The `WaitForInitialization` option controls whether the constructor blocks: - **false (default):** The constructor returns immediately and loads flags in the background. Evaluations return default values until initialization completes. - **true:** The constructor blocks until flags are loaded or `InitTimeout` expires. The `StartOffline` option allows the client to start even if initialization fails. The client will return default values and retry in the background. ### IsInitialized ```csharp bool initialized = client.IsInitialized; ``` Returns `true` once the client has successfully loaded flag data, and `false` after `Dispose()` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned. ### Variation() Generic evaluation method that works with any type: ```csharp var context = new EvaluationContext { UserId = "u-123" }; bool enabled = client.Variation("dark-mode", context, false); ``` **Signature:** ```csharp T Variation(string key, EvaluationContext context, T defaultValue) ``` (string key, EvaluationContext context, T defaultValue)"> ### BoolVariation() ```csharp bool enabled = client.BoolVariation("dark-mode", context, false); ``` ### StringVariation() ```csharp string plan = client.StringVariation("pricing-tier", context, "free"); ``` ### IntVariation() ```csharp int limit = client.IntVariation("rate-limit", context, 100); ``` ### DoubleVariation() ```csharp double ratio = client.DoubleVariation("rollout-ratio", context, 0.5); ``` ### JsonVariation() Deserializes a JSON flag value to the specified type: ```csharp var banner = client.JsonVariation( "banner-config", context, new BannerConfig { Text = "Welcome", Color = "#000" } ); ``` ( "banner-config", context, new BannerConfig { Text = "Welcome", Color = "#000" });"> ## Evaluation Details ### VariationDetail() Returns an `EvaluationDetail` object with the evaluated value and metadata about the evaluation decision. ```csharp var detail = client.VariationDetail("dark-mode", context, false); Console.WriteLine(detail.Value); // True Console.WriteLine(detail.Reason); // RuleMatch Console.WriteLine(detail.RuleId); // "rule-abc" Console.WriteLine(detail.ErrorMessage); // null ``` **Signature:** ```csharp EvaluationDetail VariationDetail(string key, EvaluationContext context, T defaultValue) ``` VariationDetail(string key, EvaluationContext context, T defaultValue)"> ### EvaluationDetail | Property | Type | Description | | --- | --- | --- | | Value | T | The evaluated flag value | | Reason | EvaluationReason | Why this value was returned | | RuleId | string? | The ID of the matched targeting rule | | ErrorMessage | string? | Error message if evaluation failed | ### EvaluationReason | Value | Description | | --- | --- | | RuleMatch | A targeting rule matched the context | | Fallthrough | No rules matched; the fallthrough variation was served | | FlagDisabled | The flag is disabled; the off variation was served | | FlagNotFound | The flag key does not exist | | Error | An error occurred during evaluation | ## Evaluation Context The `EvaluationContext` class provides built-in properties and custom attributes for [targeting rules](/docs/concepts/targeting/) and [percentage rollouts](/docs/concepts/rollout-strategies/) : ```csharp var context = new EvaluationContext { UserId = "u-123", Email = "user@example.com", Country = "US", }; // Add custom attributes context.Set("plan", "pro"); context.Set("team_size", 50); ``` ### Built-in properties | Property | Type | Description | | --- | --- | --- | | UserId | string? | Unique user identifier, used for percentage rollouts | | Email | string? | User email address | | Country | string? | User country code | ### Custom attributes Use `Set()` to add custom key-value attributes. Use `GetAttribute()` to retrieve any attribute (custom attributes take precedence over built-in properties): ```csharp context.Set("plan", "enterprise"); object? plan = context.GetAttribute("plan"); // "enterprise" ``` ## Reacting to Flag Changes ### FlagsChanged Raised when flag configuration changes, carrying the keys of the affected flags. Use it to react to a change instead of polling a value — invalidating a cache, recomputing derived state, or logging a rollout. ```csharp client.FlagsChanged += (sender, e) => { Console.WriteLine($"flags changed: {string.Join(", ", e.ChangedKeys)}"); cache.Invalidate(e.ChangedKeys); }; ``` { Console.WriteLine($"flags changed: {string.Join(", ", e.ChangedKeys)}"); cache.Invalidate(e.ChangedKeys);};"> A flag is reported when it is created, deleted, redefined, when a [segment](/docs/concepts/targeting/) its targeting rules reference changes, or when a flag it lists as a [prerequisite](/docs/concepts/prerequisites/) changes — the last resolved transitively, since a prerequisite toggle changes what its dependents evaluate to without bumping their own version. Updates are batched: one raise per update carrying every affected key, so a poll that changes 40 flags raises once. The event never fires for the initial flag load — a cold load is not a configuration change, so use `WaitForInitializationAsync()` for that — and never with an empty collection. `ChangedKeys` is unordered and duplicate-free; do not depend on its ordering. **Signature:** ```csharp event EventHandler FlagsChanged ``` FlagsChanged"> #### Handler contract Handlers run synchronously on an SDK background thread, never on the caller’s `SynchronizationContext`. Two consequences worth planning for: - **Keep handlers fast.** Delivery runs inline on the thread that received the update, so a slow handler delays subsequent flag delivery and, on the streaming path, can stall long enough to trip the idle-connection watchdog into a reconnect. Hand slow work to a queue or `Task.Run`. - **UI apps must marshal.** A WPF or WinForms handler that touches UI-owned objects directly will throw `InvalidOperationException`. Marshal back through the dispatcher first. A throwing handler is isolated: it neither stops the remaining handlers nor disturbs the polling and streaming loops, and the throw is logged at warning level. Unsubscribe with `-=`. Registering the same handler delegate twice via `+=` is a no-op — unlike standard multicast-delegate semantics, it still runs at most once per update and the first `-=` removes it. When several client handles share one underlying connection, each handle’s subscriptions are independent, so disposing one handle never revokes another’s handlers. Subscribing after disposal is a silent no-op rather than an `ObjectDisposedException`. ## Cleanup The client implements `IDisposable`. Always dispose of the client when it is no longer needed to flush events and release resources. ### Dispose() ```csharp // Option 1: using declaration (recommended) using var client = FeatureflipClient.Get("sdk-key"); // Option 2: using block using (var client = FeatureflipClient.Get("sdk-key")) { // ... } // Option 3: explicit disposal client.Dispose(); ``` ### Flush() Forces pending events to be sent to the server synchronously. Blocks for up to 5 seconds. ```csharp client.Flush(); ``` ### FlushAsync() Asynchronous version of `Flush()`: ```csharp await client.FlushAsync(cancellationToken); ``` **Signature:** ```csharp Task FlushAsync(CancellationToken cancellationToken = default) ``` ## Dependency Injection The `Featureflip.Client.Extensions.DependencyInjection` package provides extension methods for registering the client with the ASP.NET Core DI container. ### Register with SDK key ```csharp builder.Services.AddFeatureflip("sdk-dev-abc123", options => { options.BaseUrl = "https://eval.featureflip.io"; options.Streaming = true; options.WaitForInitialization = true; }); ``` { options.BaseUrl = "https://eval.featureflip.io"; options.Streaming = true; options.WaitForInitialization = true;});"> ### Register from configuration ```csharp builder.Services.AddFeatureflip( builder.Configuration.GetSection("Featureflip") ); ``` With `appsettings.json`: ```json { "Featureflip": { "SdkKey": "sdk-dev-abc123", "BaseUrl": "https://eval.featureflip.io", "Streaming": true } } ``` ### Register with options action ```csharp builder.Services.AddFeatureflip(options => { options.SdkKey = "sdk-dev-abc123"; options.BaseUrl = "https://eval.featureflip.io"; options.WaitForInitialization = true; }); ``` { options.SdkKey = "sdk-dev-abc123"; options.BaseUrl = "https://eval.featureflip.io"; options.WaitForInitialization = true;});"> ### Inject into services The client is registered as `IFeatureflipClient` (singleton): ```csharp public class MyService { private readonly IFeatureflipClient _flags; public MyService(IFeatureflipClient flags) { _flags = flags; } public void DoWork() { var context = new EvaluationContext { UserId = "u-123" }; if (_flags.BoolVariation("new-feature", context, false)) { // feature is enabled } } } ``` ## Types ### IFeatureflipClient The main interface for flag evaluation. Extends `IDisposable`. ```csharp public interface IFeatureflipClient : IDisposable { event EventHandler FlagsChanged; bool IsInitialized { get; } T Variation(string key, EvaluationContext context, T defaultValue); EvaluationDetail VariationDetail(string key, EvaluationContext context, T defaultValue); bool BoolVariation(string key, EvaluationContext context, bool defaultValue); string StringVariation(string key, EvaluationContext context, string defaultValue); int IntVariation(string key, EvaluationContext context, int defaultValue); double DoubleVariation(string key, EvaluationContext context, double defaultValue); T JsonVariation(string key, EvaluationContext context, T defaultValue); void Flush(); Task FlushAsync(CancellationToken cancellationToken = default); Task WaitForInitializationAsync(CancellationToken cancellationToken = default); } ``` FlagsChanged; bool IsInitialized { get; } T Variation(string key, EvaluationContext context, T defaultValue); EvaluationDetail VariationDetail(string key, EvaluationContext context, T defaultValue); bool BoolVariation(string key, EvaluationContext context, bool defaultValue); string StringVariation(string key, EvaluationContext context, string defaultValue); int IntVariation(string key, EvaluationContext context, int defaultValue); double DoubleVariation(string key, EvaluationContext context, double defaultValue); T JsonVariation(string key, EvaluationContext context, T defaultValue); void Flush(); Task FlushAsync(CancellationToken cancellationToken = default); Task WaitForInitializationAsync(CancellationToken cancellationToken = default);}"> ### FlagsChangedEventArgs Payload for `FlagsChanged` . ```csharp public sealed class FlagsChangedEventArgs : EventArgs { public IReadOnlyCollection ChangedKeys { get; } } ``` ChangedKeys { get; }}"> ### FeatureFlagInitializationException Thrown when the client fails to initialize. This can occur when: - The SDK key is missing or invalid - The initial flag fetch times out (and `StartOffline` is `false`) - A network error prevents flag loading (and `StartOffline` is `false`) ## See also - [C# Quickstart](/docs/quickstart/csharp/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/flutter/ # Flutter SDK Reference **TL;DR:** Add `featureflip` to `pubspec.yaml`, call `client.boolVariation('flag-key', defaultValue: false)`, and you’re evaluating flags from any Flutter app. The `featureflip` Flutter package is a single client SDK for evaluating [feature flags](/docs/concepts/feature-flags/) across every Flutter platform — iOS, Android, web, macOS, Windows, and Linux. It is a *client-side* SDK: flag values arrive pre-evaluated from the Featureflip Evaluation API, so your targeting rules stay on the server and never get bundled into your release IPA, AAB, or web build. Distribution is pub.dev as `featureflip`, with automated publishing from the canonical mirror repo via OIDC. The SDK is designed around Dart `Stream`s and the `ValueListenable` pattern, so it integrates cleanly with `StreamBuilder`, `ValueListenableBuilder`, `Provider`, `riverpod`, `bloc`, and `get_it`. A `FeatureflipBuilder` widget rebuilds your subtree whenever a flag value changes — useful when [percentage rollouts](/docs/concepts/rollout-strategies/) shift assignments while the app is running. The streaming connection is paused automatically when the app goes to the background (via `WidgetsBindingObserver`) and resumed when it returns to the foreground, conserving battery and avoiding unnecessary network use on mobile. Identification works the way you’d expect: call `identify()` after sign-in, and the active context (anchored on `userId` for stable rollout buckets) updates immediately, with [targeting rules](/docs/concepts/targeting/) re-evaluated against the new attributes. The SDK persists the latest snapshot to local storage via `shared_preferences`, so a cold start in airplane mode still resolves flags from the last successful sync rather than falling back to defaults. New to Featureflip? Start with the [Flutter quickstart](/docs/quickstart/flutter/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-flutter) ## Requirements - Flutter 3.24+ - Dart 3.5+ ## Installation Add to your `pubspec.yaml`: ```yaml dependencies: featureflip: ^2.0.0 ``` Then run: ```bash flutter pub get ``` ## Configuration The `FeatureflipConfig` class accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | clientKey | String | required | Client SDK key from your project settings | | baseUrl | String | "https://eval.featureflip.io" | Evaluation API base URL | | context | Map | {} | Initial evaluation context (user attributes) | | streaming | bool | true | Enable SSE for real-time flag updates | | pollIntervalSeconds | int | 30 | Polling interval in seconds (used when streaming is disabled) | | flushIntervalSeconds | int | 30 | Interval in seconds between event flush batches | | flushBatchSize | int | 100 | Maximum number of events per flush batch | | initTimeoutSeconds | int | 10 | Maximum time in seconds to wait for initial flag fetch | ### Example configuration ```dart final config = FeatureflipConfig( clientKey: 'your-client-sdk-key', context: {'user_id': 'u-123'}, streaming: true, pollIntervalSeconds: 60, initTimeoutSeconds: 15, ); ``` ## Initialization Obtain a client via the `get()` static factory and call `initialize()`: ```dart import 'package:featureflip/featureflip.dart'; final config = FeatureflipConfig(clientKey: 'your-client-sdk-key'); final client = FeatureflipClient.get('your-client-sdk-key', config: config); await client.initialize(); ``` `initialize()` fetches flag values from the API, starts SSE streaming (or polling), and begins the event processor. The SDK is usable after initialization completes, even if the network request fails (flags default to provided defaults). ### Lifetime `FeatureflipClient.get()` is singleton by construction — calling it multiple times with the same SDK key returns handles that share one underlying connection (refcounted). The connection shuts down only when the last handle is closed. This makes the client safe for any DI registration lifetime (singleton, scoped, or transient). ```dart final client1 = FeatureflipClient.get('key', config: config); final client2 = FeatureflipClient.get('key', config: config); // Both share one connection. close() decrements the refcount. ``` ### isInitialized ```dart client.isInitialized; // bool ``` A read-only property that returns `true` once `initialize()` has completed (regardless of whether the initial network fetch succeeded), and `false` after `close()` — a closed handle serves the caller’s default from every accessor. For test clients created with `forTesting()`, this is immediately `true`. ## Evaluating Flags All variation methods are synchronous and return immediately from an in-memory cache. They accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type. Since this is a client-side SDK, evaluation context is set at initialization or via `identify()` — it is not passed per evaluation call. ### boolVariation(key, defaultValue:) ```dart final enabled = client.boolVariation('dark-mode', defaultValue: false); ``` ### stringVariation(key, defaultValue:) ```dart final color = client.stringVariation('button-color', defaultValue: 'blue'); ``` ### numberVariation(key, defaultValue:) ```dart final limit = client.numberVariation('rate-limit', defaultValue: 100.0); ``` Returns a `double`. ### jsonVariation(key, defaultValue:) ```dart final config = client.jsonVariation('ui-config', defaultValue: {'theme': 'light'}); ``` Returns the raw `dynamic` value. ## Identifying Users Call `identify()` to re-evaluate all flags for a new user context — typically after login. This makes a network request to the evaluation API with the new context and updates all cached flag values. ```dart await client.identify({'user_id': 'u-123', 'plan': 'pro'}); ``` The streaming or polling data source is also updated with the new context. ## Event Tracking ### track(eventName, metadata:) Enqueues a custom analytics event. Events are batched and flushed automatically. ```dart client.track('purchase-completed', metadata: { 'plan': 'pro', 'amount': 29.99, }); ``` Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### flush() Forces all buffered events to be sent immediately. ```dart await client.flush(); ``` ## Cleanup ### close() Decrements the refcount on the shared connection. If this is the last handle for the SDK key, stops streaming or polling and flushes remaining events. Safe to call multiple times — subsequent calls are no-ops. ```dart await client.close(); ``` ### Lifecycle management The SDK automatically observes app lifecycle events via `WidgetsBindingObserver`: - **Background (paused)**: Stops streaming/polling and flushes events - **Foreground (resumed)**: Resumes streaming/polling No manual intervention is required. ## Flutter Widget Integration The SDK provides a `FeatureflipProvider` (a `ChangeNotifier`) for reactive Flutter widget updates. Access it via the client’s `flagProvider` property. ```dart ListenableBuilder( listenable: client.flagProvider, builder: (context, _) { final showBanner = client.flagProvider.boolVariation( 'show-banner', defaultValue: false, ); if (!showBanner) return const SizedBox.shrink(); return const Text('New feature available!'); }, ) ``` The provider notifies listeners when flag values change via streaming or `identify()`. ## Testing ### FeatureflipClient.forTesting(overrides) Creates a test client with static flag overrides. No network calls are made and no background tasks are started. The client is immediately initialized. ```dart final client = FeatureflipClient.forTesting({ 'dark-mode': true, 'pricing-tier': 'enterprise', 'rate-limit': 500, }); client.boolVariation('dark-mode', defaultValue: false); // true client.stringVariation('pricing-tier', defaultValue: 'free'); // 'enterprise' client.boolVariation('unknown', defaultValue: false); // false (default) ``` **Signature:** ```dart static FeatureflipClient forTesting(Map overrides) ``` overrides)"> Supported override value types: `bool`, `String`, `int`, `double`. ## Types ### FlagValue The internal representation of a flag returned by the server: | Property | Type | Description | | --- | --- | --- | | value | dynamic | The evaluated flag value | | variation | String | The key of the matched variation | | reason | String | Why this value was returned | ### Flag reasons | Value | Description | | --- | --- | | "Fallthrough" | No rules matched; the fallthrough variation was served | | "RuleMatch" | A targeting rule matched the context | | "FlagDisabled" | The flag is disabled; the off variation was served | | "FlagNotFound" | The flag key does not exist | | "Error" | An error occurred during evaluation | | "TEST" | Returned by test clients | ## Platform Support - iOS - Android - Web - macOS - Linux - Windows ## See also - [Flutter Quickstart](/docs/quickstart/flutter/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/go/ # Go SDK Reference **TL;DR:** Run `go get github.com/canopy-labs/featureflip-go/v2`, call `client.BoolVariation("flag-key", ctx, false)`, and you’re evaluating flags from any Go service. The `featureflip` Go package provides a goroutine-safe client for evaluating [feature flags](/docs/concepts/feature-flags/) . It requires Go 1.25+, supports real-time updates via SSE streaming, and uses the standard library’s `net/http` for all network communication with no external dependencies. New to Featureflip? Start with the [Go quickstart](/docs/quickstart/go/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-go) ## Requirements - Go 1.25+ ## Installation ```bash go get github.com/canopy-labs/featureflip-go/v2 ``` Import in your code: ```go import "github.com/canopy-labs/featureflip-go/v2" ``` All public types and functions are in the `featureflip` package. ## Configuration The client uses the functional options pattern. All options have sensible defaults. | Option | Default | Description | | --- | --- | --- | | WithBaseURL(url) | "https://eval.featureflip.io" | Base URL of the Evaluation API | | WithStreaming(bool) | true | Enable SSE for real-time flag updates | | WithPollInterval(d) | 30s | Polling interval (used when streaming is disabled) | | WithFlushInterval(d) | 30s | Interval between event flush batches | | WithFlushBatchSize(n) | 100 | Maximum number of events per flush batch | | WithInitTimeout(d) | 10s | Maximum time to wait for initial flag fetch | | WithConnectTimeout(d) | 5s | Timeout for establishing HTTP connections | | WithReadTimeout(d) | 10s | Timeout for reading HTTP responses | ### Example configuration ```go client, err := featureflip.Get("sdk-dev-abc123", featureflip.WithBaseURL("https://eval.featureflip.io"), featureflip.WithStreaming(true), featureflip.WithPollInterval(60 * time.Second), featureflip.WithFlushInterval(15 * time.Second), featureflip.WithInitTimeout(30 * time.Second), ) ``` ## Initialization Create a client by calling `Get` with an SDK key and optional configuration: ```go client, err := featureflip.Get("sdk-dev-abc123") if err != nil { log.Fatalf("Failed to initialize: %v", err) } defer client.Close() ``` `Get` blocks until the initial flag configuration is fetched (up to `initTimeout`). It returns an error if: - The SDK key is empty and `FEATUREFLIP_SDK_KEY` is not set - The initial flag fetch fails - The initialization times out ### SDK key resolution If the `sdkKey` parameter is empty, the client falls back to the `FEATUREFLIP_SDK_KEY` environment variable. ```go // SDK key read from FEATUREFLIP_SDK_KEY environment variable client, err := featureflip.Get("") ``` ### Initialized() ```go ready := client.Initialized() ``` Returns `true` once the client has successfully loaded flag data, and `false` after `Close()` — a closed handle serves the caller’s default from every accessor. Since `Get` blocks until initialization, this returns `true` for a successfully created client — until it is closed, after which it returns `false`. ## Evaluating Flags All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned. ### BoolVariation() ```go ctx := featureflip.EvaluationContext{UserID: "u-123"} enabled := client.BoolVariation("dark-mode", ctx, false) ``` **Signature:** ```go func (c *Client) BoolVariation(key string, ctx EvaluationContext, defaultValue bool) bool ``` ### StringVariation() ```go plan := client.StringVariation("pricing-tier", ctx, "free") ``` **Signature:** ```go func (c *Client) StringVariation(key string, ctx EvaluationContext, defaultValue string) string ``` ### Float64Variation() ```go limit := client.Float64Variation("rate-limit", ctx, 100.0) ``` **Signature:** ```go func (c *Client) Float64Variation(key string, ctx EvaluationContext, defaultValue float64) float64 ``` Note: JSON numbers are always deserialized as `float64` in Go. Use `Float64Variation` for all numeric flags. ### JSONVariation() Returns the raw deserialized value for JSON flags: ```go config := client.JSONVariation("banner-config", ctx, map[string]any{ "text": "Welcome", "color": "#000", }) ``` **Signature:** ```go func (c *Client) JSONVariation(key string, ctx EvaluationContext, defaultValue any) any ``` The returned value is the Go representation of the JSON value (maps, slices, strings, `float64`, `bool`, `nil`). ## Evaluation Details ### VariationDetail() Returns an `EvaluationDetail` struct with the evaluated value and metadata about the decision. ```go detail := client.VariationDetail("dark-mode", ctx, false) fmt.Println(detail.Value) // true fmt.Println(detail.Reason) // "RuleMatch" fmt.Println(detail.Variation) // "true-variation" fmt.Println(detail.RuleID) // "rule-abc" ``` **Signature:** ```go func (c *Client) VariationDetail(key string, ctx EvaluationContext, defaultValue any) EvaluationDetail ``` ### EvaluationDetail ```go type EvaluationDetail struct { Value any Variation string Reason EvaluationReason RuleID string } ``` | Field | Type | Description | | --- | --- | --- | | Value | any | The evaluated flag value | | Variation | string | The key of the matched variation | | Reason | EvaluationReason | Why this value was returned | | RuleID | string | The ID of the matched targeting rule (empty if not applicable) | ### EvaluationReason The `EvaluationReason` type is a `string` with the following constants: | Constant | Value | Description | | --- | --- | --- | | ReasonRuleMatch | "RuleMatch" | A targeting rule matched the context | | ReasonFallthrough | "Fallthrough" | No rules matched; the fallthrough variation was served | | ReasonFlagDisabled | "FlagDisabled" | The flag is disabled; the off variation was served | | ReasonFlagNotFound | "FlagNotFound" | The flag key does not exist | | ReasonError | "Error" | An error occurred during evaluation | ## Evaluation Context The `EvaluationContext` struct provides user attributes for flag evaluation. Custom attributes feed into [targeting rules](/docs/concepts/targeting/) , and `UserID` anchors [percentage rollouts](/docs/concepts/rollout-strategies/) : ```go ctx := featureflip.EvaluationContext{ UserID: "u-123", Attributes: map[string]any{ "email": "user@example.com", "country": "US", "plan": "pro", }, } ``` ### Fields | Field | Type | Description | | --- | --- | --- | | UserID | string | Unique user identifier, used for percentage rollouts | | Attributes | map[string]any | Custom attributes for targeting rules | ## Event Tracking ### Track() Records a custom analytics event. ```go metadata := map[string]any{ "plan": "pro", "amount": 29.99, } client.Track("purchase-completed", ctx, metadata) ``` **Signature:** ```go func (c *Client) Track(eventKey string, ctx EvaluationContext, metadata map[string]any) ``` Events are batched and flushed automatically based on `WithFlushInterval` and `WithFlushBatchSize`. ### Identify() Records an identify event for user association and analytics. ```go client.Identify(featureflip.EvaluationContext{ UserID: "u-123", }) ``` **Signature:** ```go func (c *Client) Identify(ctx EvaluationContext) ``` ### Flush() Forces all buffered events to be sent to the server immediately. ```go client.Flush() ``` ## Flag Change Notifications ### OnUpdate() Subscribes to flag-configuration updates. The listener gets the keys of the flags each change touched, batched into one call however many flags that was. Needs SDK 2.8.0 or newer. ```go unsubscribe := client.OnUpdate(func(keys []string) { log.Printf("flags changed: %v", keys) cache.Invalidate(keys) }) defer unsubscribe() ``` A flag is reported when it is added, removed or modified. Two more cases count as well, because each one moves a flag’s evaluated value without touching its own configuration: an edit to a [segment](/docs/concepts/targeting/) that the flag’s targeting rules reference, and a change to a flag it depends on through a [prerequisite](/docs/concepts/prerequisites/) . The initial flag load doesn’t fire the listener, and neither does a poll or reconnect that changed nothing. The listener is never called with an empty slice, and each listener receives its own copy of it. Listeners run on the SDK’s streaming or polling goroutine, so keep them short. A slow listener holds up flag delivery, and on the streaming path it can stall the connection. Do the minimum here and hand anything heavier to a channel or a worker goroutine. A panic in a listener is recovered and logged. It doesn’t stop flag delivery or the other listeners. The returned func unsubscribes, and calling it twice is harmless. Closing the client handle that registered a listener drops it too, so a handle that goes away can’t leave a listener firing on the shared client. A `nil` listener, or one registered on a closed handle, gets back a func that does nothing. **Signature:** ```go type FlagUpdateListener func(flagKeys []string) func (c *Client) OnUpdate(listener FlagUpdateListener) func() ``` ## Cleanup ### Close() Shuts down the client, stops background goroutines (streaming or polling), and flushes remaining events. Returns `nil` (the error return is reserved for future use). Safe to call multiple times. ```go err := client.Close() ``` **Signature:** ```go func (c *Client) Close() error ``` Always defer `Close()` after creating a client: ```go client, err := featureflip.Get("sdk-key") if err != nil { log.Fatal(err) } defer client.Close() ``` ## Testing ### ForTesting() Creates a client pre-populated with flag overrides. No network calls or background goroutines are started. ```go client := featureflip.ForTesting(map[string]any{ "dark-mode": true, "pricing-tier": "enterprise", "rate-limit": 500.0, }) defer client.Close() ctx := featureflip.EvaluationContext{UserID: "test-user"} client.BoolVariation("dark-mode", ctx, false) // true client.StringVariation("pricing-tier", ctx, "free") // "enterprise" ``` **Signature:** ```go func ForTesting(overrides map[string]any) *Client ``` Supported value types: `bool`, `string`, `float64`/`int`, and any JSON-serializable type. The client is fully initialized immediately and returns override values regardless of the evaluation context. ## Types Summary | Type | Description | | --- | --- | | Client | Main client for flag evaluation and event tracking | | Option | Functional option for configuring the client (func(*config)) | | EvaluationContext | User attributes for flag evaluation | | EvaluationDetail | Detailed result of a flag evaluation | | EvaluationReason | String type describing why a value was returned | ## See also - [Go Quickstart](/docs/quickstart/go/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/java/ # Java SDK Reference **TL;DR:** Add `io.featureflip:featureflip-java`, call `client.boolVariation("flag-key", context, false)`, and you’re evaluating flags — thread-safe and non-blocking. The `io.featureflip:featureflip-java` library provides a thread-safe client for evaluating [feature flags](/docs/concepts/feature-flags/) in Java applications. The SDK targets Java 11+ via Gradle toolchains, so it runs on every modern JVM — including LTS releases (11, 17, 21) and recent non-LTS builds — with no module-system gymnastics. Distribution is Maven Central under `io.featureflip:featureflip-java`. Internally, the client uses OkHttp for HTTP and SSE streaming and Jackson for JSON serialization. Both are well-known, battle-tested libraries that integrate cleanly with Spring, Micronaut, Quarkus, and Dropwizard. The streaming connection runs on a dedicated background thread; if it drops, the SDK falls back to polling every 30 seconds while continuing to serve the last known flag values. The client is built once per process via `FeatureflipClient.builder().sdkKey(...).build()` and shared across threads. All variation methods (`boolVariation`, `stringVariation`, `intVariation`, `doubleVariation`, `jsonVariation`) read from a `ConcurrentHashMap` and return without blocking, so you can call them from request handlers, schedulers, or message consumers without coordinating locks. The matching client implementation also powers the [Android SDK](/docs/sdks/android/) , which adds Kotlin coroutines and lifecycle integration on top. New to Featureflip? Start with the [Java quickstart](/docs/quickstart/java/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-java) ## Installation ### Gradle (Kotlin DSL) ```kotlin dependencies { implementation("io.featureflip:featureflip-java:2.3.0") } ``` ### Gradle (Groovy) ```groovy dependencies { implementation 'io.featureflip:featureflip-java:2.3.0' } ``` ### Maven ```xml io.featureflip featureflip-java 2.3.0 ```  io.featureflip featureflip-java 2.3.0"> ## Lifetime The Featureflip Java SDK is designed to be used as a singleton across your application. Obtain the client via the static factory: ```java FeatureflipClient client = FeatureflipClient.get("your-sdk-key"); ``` Or with custom configuration: ```java FeatureFlagConfig config = FeatureFlagConfig.builder() .baseUrl("https://eval.featureflip.io") .streaming(true) .build(); FeatureflipClient client = FeatureflipClient.get("your-sdk-key", config); ``` Calling `get()` (or `builder().build()`) multiple times with the same SDK key returns handles that share a single underlying client — the SDK maintains a process-wide cache keyed by SDK key. You cannot accidentally open duplicate streaming connections by constructing multiple clients. If you’re using a DI container like Spring Boot or Micronaut, register the client as a singleton bean: ```java @Configuration public class FeatureflipConfig { @Bean public FeatureflipClient featureflipClient(@Value("${featureflip.sdk-key}") String sdkKey) { return FeatureflipClient.get(sdkKey); } } ``` Even if you hand-roll a prototype-scoped or request-scoped registration, the factory’s deduplication ensures all resolves share one underlying client — the misregistration is neutralized, not catastrophic. ### Closing `close()` is refcounted: when multiple handles share one cached client, closing one handle does not shut down the shared background tasks. The real shutdown runs only when the last handle is closed. In a typical application this means the client lives for the entire process lifetime and is cleaned up automatically at shutdown via a `try-with-resources` block in `main` or via the DI container’s shutdown lifecycle. ## Configuration The `FeatureFlagConfig` class is built via a builder with the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | baseUrl | String | "https://eval.featureflip.io" | Base URL of the Evaluation API | | connectTimeout | Duration | 5s | Timeout for establishing connections | | readTimeout | Duration | 10s | Timeout for reading responses | | streaming | boolean | true | Enable SSE for real-time flag updates | | pollInterval | Duration | 30s | Polling interval (used when streaming is disabled) | | flushInterval | Duration | 30s | Interval between event flush batches | | flushBatchSize | int | 100 | Maximum number of events per flush batch | | initTimeout | Duration | 10s | Maximum time to wait for initialization | Configuration is applied through the client builder rather than constructed directly. ## Initialization Obtain a client via the static factory (recommended): ```java FeatureflipClient client = FeatureflipClient.get("sdk-dev-abc123"); ``` Or with configuration via the builder: ```java FeatureflipClient client = FeatureflipClient.get("sdk-dev-abc123", FeatureFlagConfig.builder() .baseUrl("https://eval.featureflip.io") .streaming(true) .initTimeout(Duration.ofSeconds(30)) .build()); ``` Both forms perform the initial flag fetch and start background streaming or polling. If the initial fetch fails, initialization continues in the background. ### waitForInitialization() Blocks until the client has loaded flag data. Throws `FeatureFlagInitializationException` if the timeout expires. ```java client.waitForInitialization(); ``` ### isInitialized() ```java boolean ready = client.isInitialized(); ``` Returns `true` once the client has successfully loaded flag data, and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ### Builder methods | Method | Parameter | Description | | --- | --- | --- | | baseUrl() | String | Set the Evaluation API base URL | | connectTimeout() | Duration | Set the connection timeout | | readTimeout() | Duration | Set the read timeout | | streaming() | boolean | Enable or disable SSE streaming | | pollInterval() | Duration | Set the polling interval | | flushInterval() | Duration | Set the event flush interval | | flushBatchSize() | int | Set the maximum events per batch | | initTimeout() | Duration | Set the initialization timeout | | build() | — | Build the client and start initialization | ## Evaluating Flags All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned. ### boolVariation() ```java EvaluationContext context = EvaluationContext.builder("u-123").build(); boolean enabled = client.boolVariation("dark-mode", context, false); ``` **Signature:** ```java boolean boolVariation(String key, EvaluationContext context, boolean defaultValue) ``` ### stringVariation() ```java String plan = client.stringVariation("pricing-tier", context, "free"); ``` **Signature:** ```java String stringVariation(String key, EvaluationContext context, String defaultValue) ``` ### intVariation() ```java int limit = client.intVariation("rate-limit", context, 100); ``` **Signature:** ```java int intVariation(String key, EvaluationContext context, int defaultValue) ``` ### doubleVariation() ```java double ratio = client.doubleVariation("rollout-ratio", context, 0.5); ``` **Signature:** ```java double doubleVariation(String key, EvaluationContext context, double defaultValue) ``` ### jsonVariation() Deserializes a JSON flag value to the specified class using Jackson: ```java BannerConfig banner = client.jsonVariation( "banner-config", context, new BannerConfig("Welcome", "#000"), BannerConfig.class ); ``` **Signature:** ```java T jsonVariation(String key, EvaluationContext context, T defaultValue, Class type) ``` T jsonVariation(String key, EvaluationContext context, T defaultValue, Class type)"> ## Evaluation Details Detail methods return an `EvaluationDetail` object with the value and metadata about the decision. ### boolVariationDetail() ```java EvaluationDetail detail = client.boolVariationDetail("dark-mode", context, false); System.out.println(detail.getValue()); // true System.out.println(detail.getReason()); // RULE_MATCH System.out.println(detail.getRuleId()); // "rule-abc" System.out.println(detail.getErrorMessage()); // null ``` detail = client.boolVariationDetail("dark-mode", context, false);System.out.println(detail.getValue()); // trueSystem.out.println(detail.getReason()); // RULE_MATCHSystem.out.println(detail.getRuleId()); // "rule-abc"System.out.println(detail.getErrorMessage()); // null"> ### stringVariationDetail() ```java EvaluationDetail detail = client.stringVariationDetail("pricing-tier", context, "free"); ``` detail = client.stringVariationDetail("pricing-tier", context, "free");"> ### intVariationDetail() ```java EvaluationDetail detail = client.intVariationDetail("rate-limit", context, 100); ``` detail = client.intVariationDetail("rate-limit", context, 100);"> ### doubleVariationDetail() ```java EvaluationDetail detail = client.doubleVariationDetail("rollout-ratio", context, 0.5); ``` detail = client.doubleVariationDetail("rollout-ratio", context, 0.5);"> ### EvaluationDetail | Method | Return type | Description | | --- | --- | --- | | getValue() | T | The evaluated flag value | | getReason() | EvaluationReason | Why this value was returned | | getRuleId() | String | The ID of the matched targeting rule (may be null) | | getErrorMessage() | String | Error message if evaluation failed (may be null) | ### EvaluationReason | Value | Description | | --- | --- | | RULE_MATCH | A targeting rule matched the context | | FALLTHROUGH | No rules matched; the fallthrough variation was served | | FLAG_DISABLED | The flag is disabled; the off variation was served | | FLAG_NOT_FOUND | The flag key does not exist | | ERROR | An error occurred during evaluation | ## Evaluation Context The `EvaluationContext` class uses a builder pattern with a required user ID. Custom attributes feed into [targeting rules](/docs/concepts/targeting/) , and the user ID anchors [percentage rollouts](/docs/concepts/rollout-strategies/) so each user gets a stable bucket assignment: ```java EvaluationContext context = EvaluationContext.builder("u-123") .set("email", "user@example.com") .set("country", "US") .set("plan", "pro") .build(); ``` ### Builder methods | Method | Parameter | Description | | --- | --- | --- | | builder() | String userId | Create a builder with the required user ID | | set() | String key, Object value | Add a custom attribute (case-insensitive keys) | | build() | — | Build the immutable context | ### Accessing attributes ```java String userId = context.getUserId(); // "u-123" Object plan = context.getAttribute("plan"); // "pro" ``` `getAttribute()` checks custom attributes first, then falls back to built-in properties (`userId`, `user_id`). ## Event Tracking ### track() Records a custom analytics event. ```java Map metadata = Map.of( "plan", "pro", "amount", 29.99 ); client.track("purchase-completed", context, metadata); ``` metadata = Map.of( "plan", "pro", "amount", 29.99);client.track("purchase-completed", context, metadata);"> **Signature:** ```java void track(String eventName, EvaluationContext context, Map metadata) ``` metadata)"> Events are batched and flushed automatically based on `flushInterval` and `flushBatchSize`. Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### identify() Records an `Identify` event for the given user. The context’s custom attributes are sent as the event’s metadata but are not stored. The event is counted per environment. This does not affect flag evaluation. [Targeting rules and segments](/docs/concepts/targeting/) are matched against the context you pass to each variation call. `identify()` does not set a context for subsequent calls, and it does not persist user attributes for later targeting. ```java client.identify(EvaluationContext.builder("123") .set("email", "user@example.com") .set("plan", "pro") .build()); ``` **Signature:** ```java void identify(EvaluationContext context) ``` ### flush() Forces all buffered events to be sent to the server immediately. ```java client.flush(); ``` ## Flag Change Notifications ### onUpdate() Subscribes to flag-configuration updates. The listener gets the keys of the flags each change touched, batched into one call however many flags that was. Needs SDK 2.9.0 or newer. ```java Runnable unsubscribe = client.onUpdate(keys -> { log.info("flags changed: {}", keys); cache.invalidateAll(keys); }); // later, when you no longer need it unsubscribe.run(); ``` { log.info("flags changed: {}", keys); cache.invalidateAll(keys);});// later, when you no longer need itunsubscribe.run();"> A flag is reported when it is added, removed or modified. Two more cases count as well, because each one moves a flag’s evaluated value without touching its own configuration: an edit to a [segment](/docs/concepts/targeting/) that the flag’s targeting rules reference, and a change to a flag it depends on through a [prerequisite](/docs/concepts/prerequisites/) . The initial flag load doesn’t fire the listener. The list it receives is never null or empty, and it comes sorted and unmodifiable. Listeners run on the SDK’s streaming or polling thread, so keep them short. A slow listener holds up flag delivery, and on the streaming path it can stall the connection. Hand anything heavier to your own executor. An exception thrown by a listener is caught and logged. It doesn’t stop flag delivery or the other listeners. The returned `Runnable` unsubscribes, and running it twice is harmless. Closing the client handle that registered a listener drops it too, so a handle that goes away can’t leave a listener firing on the shared client. Passing `null`, or subscribing on a closed handle, returns a `Runnable` that does nothing. **Signature:** ```java @FunctionalInterface public interface FlagUpdateListener { void onUpdate(List flagKeys); } public Runnable onUpdate(FlagUpdateListener listener) ``` flagKeys);}public Runnable onUpdate(FlagUpdateListener listener)"> ## Cleanup The client implements `AutoCloseable`. Use try-with-resources for automatic cleanup: ```java try (FeatureflipClient client = FeatureflipClient.get("sdk-key")) { boolean enabled = client.boolVariation("dark-mode", context, false); } // client.close() is called automatically ``` ### close() Stops SSE streaming or polling, flushes remaining events, and shuts down the executor service (with a 5-second graceful timeout). Safe to call multiple times. ```java client.close(); ``` ## Testing ### FeatureflipClient.forTesting() Creates a test client with fixed flag values. No network calls are made and no background threads are started. ```java FeatureflipClient client = FeatureflipClient.forTesting(Map.of( "dark-mode", true, "pricing-tier", "enterprise", "rate-limit", 500 )); EvaluationContext ctx = EvaluationContext.builder("test-user").build(); client.boolVariation("dark-mode", ctx, false); // true client.stringVariation("pricing-tier", ctx, "free"); // "enterprise" ``` **Signature:** ```java static FeatureflipClient forTesting(Map values) ``` values)"> The test client is fully initialized immediately. Flag values are returned regardless of the evaluation context. The values map is copied and immutable. ## Types ### FeatureFlagInitializationException Thrown by `waitForInitialization()` when the client cannot load flag data within the configured timeout. Also thrown if initialization is interrupted. ```java try { client.waitForInitialization(); } catch (FeatureFlagInitializationException e) { // Handle initialization failure log.error("Failed to initialize: {}", e.getMessage()); } ``` ### Dependencies The SDK uses the following runtime dependencies: | Library | Purpose | | --- | --- | | OkHttp 4.x | HTTP client and SSE | | Jackson 2.x | JSON serialization/deserialization | | SLF4J 2.x | Logging facade | ## See also - [Java Quickstart](/docs/quickstart/java/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/javascript/ # JavaScript / TypeScript SDK Reference **TL;DR:** Install `@featureflip/js`, call `client.boolVariation('flag-key', { user_id: 'u-123' }, false)`, and you’re evaluating flags — synchronous, never throws. The `@featureflip/js` package is the foundation of the Featureflip JavaScript / TypeScript SDK family. It evaluates [feature flags](/docs/concepts/feature-flags/) locally against an in-memory store, opens an SSE stream for real-time updates, and ships full TypeScript type definitions out of the box. Variation methods are synchronous and never throw — passing the wrong type, hitting a missing flag, or tripping over a network failure all return your supplied default value. The package is the universal layer; specific runtimes get their own thin wrapper that handles platform quirks. Use [`@featureflip/node`](/docs/sdks/nodejs/) on the server (it sets `User-Agent` and uses Node’s `fetch`/`undici`), [`@featureflip/browser`](/docs/sdks/browser/) in the browser (it routes around forbidden fetch headers and uses `EventSource` with query-string auth), and [`@featureflip/react`](/docs/sdks/react/) for React components (it adds `FeatureflipProvider`, `useFlag`, and StrictMode-safe lifecycle handling). All four packages share the same evaluation engine and event-tracking pipeline, so flag values are identical across runtimes for the same context. Initialization is idempotent: calling `initialize()` twice returns the same in-flight promise rather than re-fetching, and `close()` is safe to call multiple times. The client tracks evaluation events automatically for [targeting rules](/docs/concepts/targeting/) and [percentage rollouts](/docs/concepts/rollout-strategies/) , batching them and flushing on a configurable interval so you never block a render or a request to record an impression. New to Featureflip? Start with the [JavaScript quickstart](/docs/quickstart/javascript/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-js) ## Requirements - Node.js 20.19.0+ (for Node.js usage) or a modern browser - TypeScript 5.0+ (optional, for type definitions) ## Installation ```bash npm install @featureflip/js ``` The SDK ships with dual platform support from a single entry point — the package’s `exports` map resolves the Node build or the browser build automatically based on your runtime and bundler. Both environments import from `@featureflip/js`; pick the platform factory that matches your environment. **Node.js:** ```typescript import { FeatureflipClient, createNodePlatform } from '@featureflip/js'; ``` **Browser:** ```typescript import { FeatureflipClient, createBrowserPlatform } from '@featureflip/js'; ``` ## Configuration The `FeatureflipConfig` interface accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | sdkKey | string | required | SDK key from the Featureflip dashboard | | baseUrl | string | required | Base URL of the Evaluation API | | streaming | boolean | true | Enable SSE for real-time flag updates | | pollInterval | number | 30000 | Polling interval in milliseconds (used when streaming is disabled) | | flushInterval | number | 30000 | Interval in milliseconds between event flush batches | | flushBatchSize | number | 100 | Maximum number of events per flush batch | | initTimeout | number | 10000 | Maximum time in milliseconds to wait for initialization | | maxStreamRetries | number | 5 | Maximum SSE reconnection attempts before falling back to polling | ### Example configuration ```typescript const config: FeatureflipConfig = { sdkKey: 'sdk-dev-abc123', baseUrl: 'https://eval.featureflip.io', streaming: true, pollInterval: 60_000, flushInterval: 15_000, flushBatchSize: 50, initTimeout: 5_000, maxStreamRetries: 3, }; ``` ## Initialization Obtain a client via the static factory `FeatureflipClient.get`. There is no public constructor — the factory dedupes by SDK key, so every call with the same key returns a handle pointing at one shared underlying client (see Lifetime ). ```typescript import { FeatureflipClient, createNodePlatform } from '@featureflip/js'; const client = FeatureflipClient.get( { sdkKey: 'sdk-dev-abc123', baseUrl: 'https://eval.featureflip.io', }, createNodePlatform(), ); // Wait for flags to load before evaluating await client.waitForInitialization(); ``` ## Lifetime The JS SDK is singleton-by-construction. `FeatureflipClient.get(config, platform)` is the only way to obtain a client, and the factory caches one shared core per SDK key: - First `get()` for a given SDK key constructs the shared core (platform, HTTP client, SSE connection, event processor), kicks off initialization, and returns a handle with refcount 1. - Subsequent `get()` calls with the same SDK key return a new handle and increment the refcount. The `config` and `platform` arguments are ignored on repeat calls; if the config differs meaningfully, a warning is logged and the cached instance is reused. - `handle.close()` decrements the refcount. When the refcount reaches zero, the shared core runs the real shutdown: closes the SSE connection, flushes pending events, clears timers, and removes itself from the factory cache. - Double-closing the same handle is a no-op. - Different SDK keys construct independent shared cores — multi-tenant use is fine. This makes the SDK safe under any lifetime pattern, including per-request handler construction and scoped/transient DI registration — the extra `get()` calls allocate a cheap handle, not a new HTTP client or SSE stream. For tests that need a standalone instance not registered in the cache, use `FeatureflipClient.forTesting(flags)`. ### waitForInitialization() ```typescript await client.waitForInitialization(): Promise ``` "> Blocks until the client has fetched and stored the initial flag configuration. Rejects with an error if the fetch does not complete within `initTimeout` milliseconds. ### isInitialized ```typescript client.isInitialized: boolean ``` A read-only property that returns `true` once the client has successfully loaded flag data, and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned. ### boolVariation() ```typescript const enabled = client.boolVariation('dark-mode', { user_id: 'u-123' }, false); ``` **Signature:** ```typescript boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): boolean ``` ### stringVariation() ```typescript const plan = client.stringVariation('pricing-tier', { user_id: 'u-123' }, 'free'); ``` **Signature:** ```typescript stringVariation(key: string, context: EvaluationContext, defaultValue: string): string ``` ### numberVariation() ```typescript const limit = client.numberVariation('rate-limit', { user_id: 'u-123' }, 100); ``` **Signature:** ```typescript numberVariation(key: string, context: EvaluationContext, defaultValue: number): number ``` ### jsonVariation() ```typescript interface BannerConfig { text: string; color: string; } const banner = client.jsonVariation( 'banner-config', { user_id: 'u-123' }, { text: 'Welcome', color: '#000' }, ); ``` ( 'banner-config', { user_id: 'u-123' }, { text: 'Welcome', color: '#000' },);"> **Signature:** ```typescript jsonVariation(key: string, context: EvaluationContext, defaultValue: T): T ``` (key: string, context: EvaluationContext, defaultValue: T): T"> ## Evaluation Details Use `variationDetail()` when you need to understand why a particular value was returned. ### variationDetail() ```typescript const detail = client.variationDetail('dark-mode', { user_id: 'u-123' }, false); console.log(detail.value); // true console.log(detail.reason); // 'RuleMatch' console.log(detail.ruleId); // 'rule-abc' ``` **Signature:** ```typescript variationDetail(key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail ``` (key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail"> **Returns** an `EvaluationDetail` object: | Property | Type | Description | | --- | --- | --- | | value | T | The evaluated flag value | | reason | EvaluationReason | Why this value was returned | | variationKey | string | undefined | The key of the matched variation | | ruleId | string | undefined | The ID of the matched targeting rule | ### EvaluationReason | Value | Description | | --- | --- | | 'RuleMatch' | A targeting rule matched the context | | 'Fallthrough' | No rules matched; the fallthrough variation was served | | 'FlagDisabled' | The flag is disabled; the off variation was served | | 'FlagNotFound' | The flag key does not exist | | 'Error' | An error occurred during evaluation | ## Event Tracking ### track() Records a custom analytics event. ```typescript client.track('purchase-completed', { user_id: 'u-123' }, { plan: 'pro', amount: 29.99, }); ``` **Signature:** ```typescript track(eventKey: string, context: EvaluationContext, metadata?: Record): void ``` ): void"> Events are batched and flushed automatically based on `flushInterval` and `flushBatchSize`. Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ## Reacting to Flag Changes ### on() Subscribes to flag-configuration updates. The listener receives the keys of the flags affected by each change, batched into one call however many flags it touched. ```typescript const unsubscribe = client.on('update', (keys) => { console.log('flags changed:', keys); }); ``` { console.log('flags changed:', keys);});"> A flag is reported when it is created, deleted, redefined, or when a [segment](/docs/concepts/targeting/) its targeting rules reference changes. The listener does **not** fire for the initial flag load — use `waitForInitialization()` for that — and is never called with an empty array. This holds for both data sources: an SSE delta reports the flag it carried, and a poll reports only what actually differs from the previous snapshot. Returns an unsubscribe function. Listeners are also dropped automatically when the handle that registered them is closed. Listener errors are caught and logged, so a throwing listener neither breaks flag delivery nor stops the remaining listeners. **Signature:** ```typescript on(event: 'update', listener: (keys: string[]) => void): () => void ``` void): () => void"> ### off() Removes a listener previously registered with `on()`. Equivalent to calling the unsubscribe function `on()` returned. ```typescript client.off('update', listener); ``` ## Cleanup ### close() Shuts down the client, flushes any pending events, stops SSE streaming or polling, and releases resources. ```typescript await client.close(); ``` **Signature:** ```typescript close(): Promise ``` "> Always call `close()` when the client is no longer needed to ensure events are delivered. ## Testing ### FeatureflipClient.forTesting() Creates a test client with hardcoded flag values. No network calls are made and no background processes are started. ```typescript const client = FeatureflipClient.forTesting({ 'dark-mode': true, 'pricing-tier': 'enterprise', 'rate-limit': 500, }); // Evaluations return the test values client.boolVariation('dark-mode', {}, false); // true client.stringVariation('pricing-tier', {}, 'free'); // 'enterprise' ``` **Signature:** ```typescript static forTesting(flags: Record): FeatureflipClient ``` ): FeatureflipClient"> The test client is fully initialized immediately and returns the provided values regardless of the evaluation context. ## Types ### EvaluationContext ```typescript type EvaluationContext = Record; ``` ;"> A plain object containing user attributes for flag evaluation. The `user_id` property is used for percentage rollouts and event tracking. ```typescript const context: EvaluationContext = { user_id: 'u-123', email: 'user@example.com', country: 'US', plan: 'pro', }; ``` ### EvaluationDetail ```typescript interface EvaluationDetail { value: T; variationKey?: string; reason: EvaluationReason; ruleId?: string; } ``` { value: T; variationKey?: string; reason: EvaluationReason; ruleId?: string;}"> ### FlagType ```typescript type FlagType = 'Boolean' | 'String' | 'Number' | 'Json'; ``` ### FeatureflipConfig ```typescript interface FeatureflipConfig { sdkKey: string; baseUrl: string; streaming?: boolean; pollInterval?: number; flushInterval?: number; flushBatchSize?: number; initTimeout?: number; maxStreamRetries?: number; } ``` ## Platform Differences | Feature | Node.js | Browser | | --- | --- | --- | | SSE headers | Custom Authorization header via eventsource library | SDK key passed as query parameter (browser EventSource does not support custom headers) | | User-Agent | Set to featureflip-js/1.0.0 | Not set (browser fetch forbids User-Agent) | | MD5 hashing | Node.js crypto module | Built-in pure-JS implementation | ## See also - [JavaScript Quickstart](/docs/quickstart/javascript/) — get started in under 5 minutes - [Node.js SDK](/docs/sdks/nodejs/) — Node.js-specific wrapper with simpler setup - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/nodejs/ # Node.js SDK Reference **TL;DR:** Install `@featureflip/node`, call `client.boolVariation('flag-key', { user_id: 'u-123' }, false)`, and you’re evaluating flags from any Node service. The `@featureflip/node` package is the server-side wrapper around `@featureflip/js`, tuned specifically for Node.js processes. It pre-wires the Node fetch implementation, sets the SDK’s `User-Agent` (a header that browser fetch forbids), and configures connection pooling so a single long-lived client can handle a high request rate without leaking sockets. Node 20.19.0 or later is required — earlier versions ship a fetch implementation that doesn’t handle SSE reconnection cleanly. Use this package on any Node.js workload: Express/Fastify/Koa/Hapi servers, AWS Lambda, Vercel serverless functions, BullMQ workers, NestJS microservices, or scheduled scripts. [Feature flags](/docs/concepts/feature-flags/) are evaluated locally — there is no per-request network call to Featureflip — so adding flag checks to a hot endpoint costs roughly a `Map.get`. The streaming connection runs on a background timer and surfaces config updates within milliseconds, so a [percentage rollout](/docs/concepts/rollout-strategies/) toggled in the dashboard takes effect across your fleet without a deploy. The Node SDK is **singleton-by-construction**: there is no public constructor, only the static factory `FeatureflipClient.get(config)`. Every call with the same SDK key returns a handle pointing at the same underlying shared client. You can call `get()` from anywhere in your process — inside a request handler, inside a DI container, inside a library — and the SDK guarantees you only ever get one set of background tasks and one SSE streaming connection per key. See the Lifetime section below for the full contract. For advanced [targeting rules](/docs/concepts/targeting/) , pass an evaluation context with arbitrary attributes — anything serializable as JSON works. New to Featureflip? Start with the [Node.js quickstart](/docs/quickstart/nodejs/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-node) ## Requirements - Node.js 20.19.0+ ## Installation ```bash npm install @featureflip/node ``` ## Configuration The `NodeFeatureflipConfig` interface accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | sdkKey | string | required | SDK key from the Featureflip dashboard | | baseUrl | string | "https://eval.featureflip.io" | Base URL of the Evaluation API | | streaming | boolean | true | Enable SSE for real-time flag updates | | pollInterval | number | 30000 | Polling interval in milliseconds (used when streaming is disabled) | | flushInterval | number | 30000 | Interval in milliseconds between event flush batches | | flushBatchSize | number | 100 | Maximum number of events per flush batch | | initTimeout | number | 10000 | Maximum time in milliseconds to wait for initialization | | maxStreamRetries | number | 5 | Maximum SSE reconnection attempts before falling back to polling | ### Example configuration ```typescript const client = FeatureflipClient.get({ sdkKey: 'sdk-dev-abc123', streaming: true, pollInterval: 60_000, flushInterval: 15_000, flushBatchSize: 50, initTimeout: 5_000, }); ``` ## Initialization ### FeatureflipClient.get() The primary entry point. Synchronously returns a handle; the shared core loads flag configuration in the background. ```typescript import { FeatureflipClient } from '@featureflip/node'; const client = FeatureflipClient.get({ sdkKey: 'sdk-dev-abc123', }); await client.waitForInitialization(); ``` Calling `get()` multiple times with the same SDK key returns different handles backed by the same shared client. See Lifetime . ### FeatureflipClient.create() Convenience helper that calls `get()` and awaits initialization before returning. ```typescript const client = await FeatureflipClient.create({ sdkKey: 'sdk-dev-abc123', }); ``` `waitForInitialization()` rejects with an error if the fetch does not complete within `initTimeout` milliseconds. ### isInitialized ```typescript client.isInitialized: boolean ``` A read-only property that returns `true` once the client has successfully loaded flag data, and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned. ### boolVariation() ```typescript const enabled = client.boolVariation('dark-mode', { user_id: 'u-123' }, false); ``` **Signature:** ```typescript boolVariation(key: string, context: EvaluationContext, defaultValue: boolean): boolean ``` ### stringVariation() ```typescript const plan = client.stringVariation('pricing-tier', { user_id: 'u-123' }, 'free'); ``` **Signature:** ```typescript stringVariation(key: string, context: EvaluationContext, defaultValue: string): string ``` ### numberVariation() ```typescript const limit = client.numberVariation('rate-limit', { user_id: 'u-123' }, 100); ``` **Signature:** ```typescript numberVariation(key: string, context: EvaluationContext, defaultValue: number): number ``` ### jsonVariation() ```typescript interface BannerConfig { text: string; color: string; } const banner = client.jsonVariation( 'banner-config', { user_id: 'u-123' }, { text: 'Welcome', color: '#000' }, ); ``` ( 'banner-config', { user_id: 'u-123' }, { text: 'Welcome', color: '#000' },);"> **Signature:** ```typescript jsonVariation(key: string, context: EvaluationContext, defaultValue: T): T ``` (key: string, context: EvaluationContext, defaultValue: T): T"> ## Evaluation Details Use `variationDetail()` when you need to understand why a particular value was returned. ### variationDetail() ```typescript const detail = client.variationDetail('dark-mode', { user_id: 'u-123' }, false); console.log(detail.value); // true console.log(detail.reason); // 'RuleMatch' console.log(detail.ruleId); // 'rule-abc' ``` **Signature:** ```typescript variationDetail(key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail ``` (key: string, context: EvaluationContext, defaultValue: T): EvaluationDetail"> **Returns** an `EvaluationDetail` object: | Property | Type | Description | | --- | --- | --- | | value | T | The evaluated flag value | | reason | EvaluationReason | Why this value was returned | | variationKey | string | undefined | The key of the matched variation | | ruleId | string | undefined | The ID of the matched targeting rule | ### EvaluationReason | Value | Description | | --- | --- | | 'RuleMatch' | A targeting rule matched the context | | 'Fallthrough' | No rules matched; the fallthrough variation was served | | 'FlagDisabled' | The flag is disabled; the off variation was served | | 'FlagNotFound' | The flag key does not exist | | 'Error' | An error occurred during evaluation | ## Event Tracking ### track() Records a custom analytics event. ```typescript client.track('purchase-completed', { user_id: 'u-123' }, { plan: 'pro', amount: 29.99, }); ``` **Signature:** ```typescript track(eventKey: string, context: EvaluationContext, metadata?: Record): void ``` ): void"> Events are batched and flushed automatically based on `flushInterval` and `flushBatchSize`. Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### identify() Records an `Identify` event for the given user. The event is counted per environment, and the attributes you pass are not stored. This does not affect flag evaluation. [Targeting rules and segments](/docs/concepts/targeting/) are matched against the context you pass to each variation call. `identify()` does not set a context for subsequent calls, and it does not persist user attributes for later targeting. ```typescript client.identify({ user_id: 'u-123', email: 'user@example.com', plan: 'pro' }); ``` **Signature:** ```typescript identify(context: EvaluationContext): void ``` ## Reacting to Flag Changes ### on() Subscribes to flag-configuration updates. The listener receives the keys of the flags affected by each change, batched into one call however many flags it touched. ```typescript const unsubscribe = client.on('update', (keys) => { console.log('flags changed:', keys); cache.invalidate(keys); }); ``` { console.log('flags changed:', keys); cache.invalidate(keys);});"> A flag is reported when it is created, deleted, redefined, or when a [segment](/docs/concepts/targeting/) its targeting rules reference changes. The listener does **not** fire for the initial flag load — use `waitForInitialization()` for that — and is never called with an empty array. Returns an unsubscribe function. Listeners registered through a handle are also dropped automatically when that handle is closed, so a handle that goes away cannot leave a listener firing off the shared core. **Signature:** ```typescript on(event: 'update', listener: (keys: string[]) => void): () => void ``` void): () => void"> ### off() Removes a listener previously registered with `on()`. Equivalent to calling the unsubscribe function `on()` returned. ```typescript client.off('update', listener); ``` Listener errors are caught and logged — a throwing listener neither breaks flag delivery nor stops the remaining listeners. ## Lifetime The Node SDK is singleton-by-construction: you cannot `new` a client directly, and every `FeatureflipClient.get(config)` call with the same SDK key returns a handle pointing at one shared background client. This is intentional — a Featureflip client maintains a long-lived SSE streaming connection, a periodic event flush loop, and an in-memory flag store. Opening more than one per SDK key per process wastes server resources and causes update delivery to fan out unpredictably. The factory **refcounts** the shared core: - The first `get()` for a given SDK key constructs the shared core, kicks off initialization, and returns a handle with refcount `1`. - Subsequent `get()` calls with the same SDK key return a new handle and increment the refcount. The `config` argument on repeat calls is ignored; if it differs meaningfully from the first call’s config, a warning is logged. - `handle.close()` decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: closes the SSE connection, flushes any pending events, and removes itself from the factory cache. - Double-closing the same handle is a no-op — the handle tracks its own disposed state, so calling `close()` twice does not double-decrement the refcount. - A different SDK key always constructs an independent shared core. Multi-tenant scenarios — one process serving flags for multiple environments — work naturally. This makes the SDK safe under any lifetime pattern: - **Singleton at startup** (the recommended pattern): call `get()` once in your bootstrap code, inject the handle everywhere, `close()` it on `SIGTERM`. The refcount stays at 1 for the life of the process. - **Per-request handler in Express/Fastify/NestJS**: calling `get()` on every request is harmless. Each request gets a fresh handle on the same underlying shared core — no new HTTP fetch, no new SSE connection, no new background timers. - **Per-scope DI registration**: scoped or transient registration is no longer catastrophic. Each scope resolves to a new handle sharing the same core. - **Serverless warm invocations**: a warm Lambda or Vercel Function reuses the cached core across invocations automatically. Cold starts construct a fresh core per process, as expected. Use `FeatureflipClient.forTesting(flags)` to obtain a standalone test client that is **not** registered in the factory cache — each call returns an independent instance with no background connections. ## Cleanup ### close() Decrements the refcount on the shared core. When the last handle for a given SDK key is closed, the shared core flushes pending events, closes the SSE connection, stops the polling timer, and removes itself from the factory cache. ```typescript await client.close(); ``` **Signature:** ```typescript close(): Promise ``` "> Call `close()` during graceful shutdown (e.g., in a `SIGTERM` handler) to ensure buffered analytics events are delivered before the process exits. Calling `close()` on a handle that has already been closed is a no-op — the handle tracks its own state and will not double-decrement the refcount. ### flush() Forces all buffered events to be sent to the server immediately. ```typescript await client.flush(); ``` **Signature:** ```typescript flush(): Promise ``` "> ## Testing ### FeatureflipClient.forTesting() Creates a test client with hardcoded flag values. No network calls are made and no background processes are started. ```typescript const client = FeatureflipClient.forTesting({ 'dark-mode': true, 'pricing-tier': 'enterprise', 'rate-limit': 500, }); // Evaluations return the test values client.boolVariation('dark-mode', {}, false); // true client.stringVariation('pricing-tier', {}, 'free'); // 'enterprise' ``` **Signature:** ```typescript static forTesting(flags: Record): FeatureflipClient ``` ): FeatureflipClient"> The test client is fully initialized immediately and returns the provided values regardless of the evaluation context. ## Types ### EvaluationContext ```typescript type EvaluationContext = Record; ``` ;"> A plain object containing user attributes for flag evaluation. The `user_id` property is used for percentage rollouts and event tracking. ### EvaluationDetail ```typescript interface EvaluationDetail { value: T; variationKey?: string; reason: EvaluationReason; ruleId?: string; } ``` { value: T; variationKey?: string; reason: EvaluationReason; ruleId?: string;}"> ### NodeFeatureflipConfig ```typescript interface NodeFeatureflipConfig { sdkKey: string; baseUrl?: string; streaming?: boolean; pollInterval?: number; flushInterval?: number; flushBatchSize?: number; initTimeout?: number; maxStreamRetries?: number; } ``` ## Differences from @featureflip/js The Node SDK is a thin wrapper around `@featureflip/js`. Key differences: | | @featureflip/node | @featureflip/js | | --- | --- | --- | | Platform setup | Automatic (Node.js) | Manual (createNodePlatform() or createBrowserPlatform()) | | Base URL | Defaults to https://eval.featureflip.io | Required | | Static factory | FeatureflipClient.create() | Not available | Use `@featureflip/node` for Node.js applications. Use `@featureflip/js` directly if you need to support both Node.js and browser from the same codebase. ## See also - [Node.js Quickstart](/docs/quickstart/nodejs/) — get started in under 5 minutes - [Using this SDK in Next.js](/blog/feature-flags-in-nextjs/) — evaluating in Server Components and `getServerSideProps`, plus the Edge-runtime caveat - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/overview/ # SDK Overview Featureflip provides **server-side** and **client-side** SDKs. Pick the one that matches where your code runs. ## Which SDK should I use? **Are you evaluating flags in a backend service (Node.js, Python, C#, Java, Go, PHP, Ruby)?** → Use a server-side SDK . **Are you evaluating flags in a browser or frontend app?** → Using React? Use the [React SDK](/docs/sdks/react/) . → Any other framework (or no framework)? Use the [JavaScript / TypeScript SDK](/docs/sdks/browser/) . **Are you evaluating flags in a mobile app?** → iOS / macOS / tvOS / watchOS? Use the [Swift SDK](/docs/sdks/swift/) . → Flutter? Use the [Flutter SDK](/docs/sdks/flutter/) . → Android? Use the [Android SDK](/docs/sdks/android/) . ## Client-side vs server-side | | Server-side | Client-side | | --- | --- | --- | | Use case | Backend services, APIs, server-rendered pages | Browser apps, SPAs, frontend frameworks | | SDK key | Server SDK key — keep secret, never expose to users | Client SDK key — safe to embed in frontend code | | Flag visibility | All flags in the project | Only flags marked Client-side visible | | Evaluation context | Multi-user — pass a different context per evaluation call | Single-user — set context once, call identify() on login | | Real-time updates | SSE streaming or polling (PHP polls only — see below) | SSE streaming | | Security | Key stays on your server | Key is public by design — only exposes client-visible flag values | ### SDK keys Server-side and client-side SDKs use different keys. You can find both in your project settings. - **Server SDK keys** have access to all flags and should never be included in client-side bundles. - **Client SDK keys** only return flags explicitly marked as “Client-side visible” in the dashboard. They are safe to ship in browser code. ## Client-side SDK quick start ### JavaScript / TypeScript SDK ```bash npm install @featureflip/browser ``` ```ts import { FeatureflipClient } from '@featureflip/browser'; const client = FeatureflipClient.get({ clientKey: 'your-client-sdk-key', }); await client.initialize(); const showBanner = client.boolVariation('show-banner', false); ``` See the full [JavaScript / TypeScript SDK reference](/docs/sdks/browser/) . ### React SDK ```bash npm install @featureflip/react @featureflip/browser ``` ```tsx import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react'; function App() { return ( ); } function Banner() { const showBanner = useFeatureFlag('show-banner', false); if (!showBanner) return null; return
New feature available!
; } ```    );}function Banner() { const showBanner = useFeatureFlag('show-banner', false); if (!showBanner) return null; return New feature available! ;}"> See the full [React SDK reference](/docs/sdks/react/) . ### Swift SDK In Xcode: **File → Add Package Dependencies**, then enter the repository URL. ```swift import Featureflip let config = FeatureflipConfig(clientKey: "your-client-sdk-key") let client = FeatureflipClient(config: config) await client.initialize() let showBanner = client.boolVariation("show-banner", default: false) ``` See the full [Swift SDK reference](/docs/sdks/swift/) . ### Android SDK ```kotlin dependencies { implementation("io.featureflip:featureflip-android:2.3.0") } ``` ```kotlin import io.featureflip.android.FeatureflipClient import io.featureflip.android.FeatureflipConfig val config = FeatureflipConfig(clientKey = "your-client-sdk-key") val client = FeatureflipClient.create(config) client.initialize() val showBanner = client.boolVariation("show-banner", false) ``` See the full [Android SDK reference](/docs/sdks/android/) . ## Server-side SDKs These SDKs use **server SDK keys** and are intended for backends or other controlled environments. - [Node.js](/docs/sdks/nodejs/) - [Python](/docs/sdks/python/) - [C#](/docs/sdks/csharp/) - [Java](/docs/sdks/java/) - [Go](/docs/sdks/go/) - [PHP](/docs/sdks/php/) - [Ruby](/docs/sdks/ruby/) ## Client-side SDKs - [JavaScript / TypeScript](/docs/sdks/browser/) — framework-agnostic, works with any frontend - [React SDK](/docs/sdks/react/) — React bindings with provider and hooks - [Swift SDK](/docs/sdks/swift/) — iOS, macOS, tvOS, and watchOS - [Flutter SDK](/docs/sdks/flutter/) — cross-platform Flutter apps - [Android SDK](/docs/sdks/android/) — Android and Kotlin/JVM ## OpenFeature Prefer the vendor-neutral [OpenFeature](https://openfeature.dev/) API? [Providers](/docs/integrations/openfeature/) wrap the Node.js, .NET, Python, Go, Java and PHP SDKs behind the OpenFeature standard. ## See also - [What Are Feature Flags?](/docs/concepts/feature-flags/) — understand flag types and variations - [Reliability & Resilience](/docs/concepts/reliability/) — how the SDKs behave when Featureflip is slow or unavailable - [Environments](/docs/concepts/environments/) — how SDK keys connect to environments - [Evaluation API](/docs/api/overview/) — REST API reference for building custom integrations - [How to Manage Environments](/docs/guides/environments/) — create environments and SDK keys --- ## Source: https://featureflip.io/docs/sdks/php/ # PHP SDK Reference **TL;DR:** Run `composer require featureflip/featureflip-php`, call `$client->boolVariation('flag-key', ['user_id' => 'u-123'], false)`, and you’re evaluating flags from PHP. The `featureflip/featureflip-php` package is a server-side PHP SDK for evaluating [feature flags](/docs/concepts/feature-flags/) locally — no per-request callouts to Featureflip. The SDK requires PHP 8.2 or later and is built around PSR standards: bring your own PSR-18 HTTP client, PSR-17 request factories, and PSR-16 simple cache, and the SDK plugs into them. This means it works cleanly with Laravel, Symfony, Slim, Mezzio, WordPress, Drupal, or any framework that already has Guzzle/HTTPlug and `psr/cache` in the autoloader. PHP’s request-per-process model makes long-lived in-memory clients impractical, so the SDK is built around a cache-first strategy. On first use it fetches the full flag configuration once and writes it through to your PSR-16 cache; subsequent requests in the same minute (or however long your TTL is) read from cache without a network round trip. PSR-16 reserved characters (`{}()/\@:`) are escaped automatically in cache keys so the SDK plays nicely with Redis, Memcached, APCu, and filesystem cache backends. PascalCase enum values from the API (operators, evaluation reasons) are normalized via regex on the client side, so you compare against snake_case literals in your code. The SDK’s classes are `final`, so don’t try to mock them — use the built-in `forTesting()` factory instead, which returns a client backed by flag values you supply and makes no network calls. Custom evaluation context is a plain associative array, the same shape used everywhere else in PHP. [Targeting rules](/docs/concepts/targeting/) and [percentage rollouts](/docs/concepts/rollout-strategies/) work identically to the other server SDKs, using the `user_id` key as the rollout bucket anchor. New to Featureflip? Start with the [PHP quickstart](/docs/quickstart/php/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-php) ## Installation ```bash composer require featureflip/featureflip-php ``` The package requires PSR-18 (HTTP client), PSR-17 (HTTP factories), and PSR-16 (simple cache) implementations. For example, with Guzzle and Symfony Cache: ```bash composer require guzzlehttp/guzzle symfony/cache ``` ## Configuration The `Config` class accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | baseUrl | string | "https://eval.featureflip.io" | Base URL of the Evaluation API | | pollInterval | int | 30 | How often the SDK refreshes its flag configuration. Not a cache lifetime — a fetched configuration is kept and served until a fetch succeeds in replacing it | | flushInterval | int | 30 | Ship queued events at least this often, even if the batch is not full | | flushBatchSize | int | 100 | Ship queued events once this many have accumulated | | cache | CacheInterface | null | PSR-16 simple cache implementation (required) | | httpClient | ClientInterface | null | PSR-18 HTTP client (required) | | requestFactory | RequestFactoryInterface | null | PSR-17 request factory (required) | | streamFactory | StreamFactoryInterface | null | PSR-17 stream factory (required) | | inspectors | array | [] | Callables invoked with an EvaluationEvent on every variation call | | logger | LoggerInterface | null | PSR-3 logger for config-load failures — an unreachable Evaluation API, a rejected SDK key, a skipped flag. Defaults to PHP’s error log | ### Quick example with Guzzle and Symfony Cache ```php use Featureflip\FeatureflipClient; use Featureflip\Config; use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Psr7\HttpFactory; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\Cache\Adapter\FilesystemAdapter; $cache = new Psr16Cache(new FilesystemAdapter()); $guzzle = new Guzzle(); $factory = new HttpFactory(); $config = new Config( baseUrl: 'https://eval.featureflip.io', cache: $cache, httpClient: $guzzle, requestFactory: $factory, streamFactory: $factory, ); $client = FeatureflipClient::get('your-sdk-key', $config); ``` ## Initialization Create a client by calling `FeatureflipClient::get()` with an SDK key and configuration: ```php $client = FeatureflipClient::get('sdk-dev-abc123', $config); ``` The factory fetches the initial flag configuration synchronously if the cached configuration is due a refresh and a recent failure is not still backing off. The same check runs on the evaluation path, so a client held across many requests — Laravel Octane, RoadRunner, FrankenPHP, Swoole — keeps picking up flag changes rather than freezing at the configuration it booted with. A shutdown function is registered to flush any events still queued when the PHP process exits. ### Required PSR implementations The SDK requires all four PSR dependencies to be provided: - `CacheInterface` (PSR-16) — for caching flag configurations between requests - `ClientInterface` (PSR-18) — for making HTTP requests to the Evaluation API - `RequestFactoryInterface` (PSR-17) — for creating HTTP request objects - `StreamFactoryInterface` (PSR-17) — for creating HTTP request body streams An `\InvalidArgumentException` is thrown if any of these are missing. ## Lifetime `FeatureflipClient::get()` is a singleton-by-construction factory. Calling it multiple times with the same SDK key returns handles that share one underlying connection and flag store. This design makes the SDK safe to use with any DI container lifetime (singleton, scoped, or transient). Each `close()` call decrements an internal reference count. The underlying resources (HTTP client, cache, event processor) are only cleaned up when the last handle is closed. Double-closing a handle is a safe no-op. ```php $h1 = FeatureflipClient::get('sdk-key', $config); $h2 = FeatureflipClient::get('sdk-key'); // Shares the same core — no config needed $h1->close(); // Core stays alive (h2 still holds a reference) $h2->close(); // Last handle — core shuts down ``` close(); // Core stays alive (h2 still holds a reference)$h2->close(); // Last handle — core shuts down"> ## Evaluating Flags All evaluation methods accept a flag key, an associative array context, and a typed default value. If the flag is not found or evaluation fails, the default value is returned. These methods never throw exceptions. ### boolVariation() ```php $enabled = $client->boolVariation('dark-mode', ['user_id' => 'u-123'], false); ``` boolVariation('dark-mode', ['user_id' => 'u-123'], false);"> **Signature:** ```php public function boolVariation(string $key, array $context, bool $default): bool ``` ### stringVariation() ```php $plan = $client->stringVariation('pricing-tier', ['user_id' => 'u-123'], 'free'); ``` stringVariation('pricing-tier', ['user_id' => 'u-123'], 'free');"> **Signature:** ```php public function stringVariation(string $key, array $context, string $default): string ``` ### numberVariation() ```php $limit = $client->numberVariation('rate-limit', ['user_id' => 'u-123'], 100); ``` numberVariation('rate-limit', ['user_id' => 'u-123'], 100);"> **Signature:** ```php public function numberVariation(string $key, array $context, int|float $default): int|float ``` ### jsonVariation() Returns a decoded associative array for JSON flags: ```php $config = $client->jsonVariation('banner-config', ['user_id' => 'u-123'], [ 'text' => 'Welcome', 'color' => '#000', ]); ``` jsonVariation('banner-config', ['user_id' => 'u-123'], [ 'text' => 'Welcome', 'color' => '#000',]);"> **Signature:** ```php public function jsonVariation(string $key, array $context, array $default): array ``` ## Evaluation Details ### variationDetail() Returns an `EvaluationDetail` object with the evaluated value and metadata about the decision. ```php $detail = $client->variationDetail('dark-mode', ['user_id' => 'u-123'], false); echo $detail->value; // true echo $detail->reason; // "RULE_MATCH" echo $detail->ruleId; // "rule-abc" echo $detail->variationKey; // "true-variation" ``` variationDetail('dark-mode', ['user_id' => 'u-123'], false);echo $detail->value; // trueecho $detail->reason; // "RULE_MATCH"echo $detail->ruleId; // "rule-abc"echo $detail->variationKey; // "true-variation""> **Signature:** ```php public function variationDetail(string $key, array $context, mixed $default): EvaluationDetail ``` ### EvaluationDetail A value object containing the evaluation result: | Property | Type | Description | | --- | --- | --- | | value | mixed | The evaluated flag value | | reason | string | Why this value was returned | | ruleId | string|null | The ID of the matched targeting rule, if applicable | | variationKey | string|null | The key of the matched variation | | prerequisiteKey | string|null | The prerequisite that failed, set only when reason is PREREQUISITE_FAILED | ### Evaluation reasons | Value | Description | | --- | --- | | FALLTHROUGH | No rules matched; the fallthrough variation was served | | RULE_MATCH | A targeting rule matched the context | | FLAG_DISABLED | The flag is disabled; the off variation was served | | FLAG_NOT_FOUND | The flag key does not exist | | PREREQUISITE_FAILED | A prerequisite flag was unsatisfied; the off variation was served. prerequisiteKey names it | | ERROR | Evaluation could not complete, or the handle is closed; the value is the default you passed | ## Event Tracking ### track() Records a custom analytics event. ```php $client->track('purchase-completed', ['user_id' => 'u-123'], [ 'plan' => 'pro', 'amount' => 29.99, ]); ``` track('purchase-completed', ['user_id' => 'u-123'], [ 'plan' => 'pro', 'amount' => 29.99,]);"> **Signature:** ```php public function track(string $eventKey, array $context, array $metadata = []): void ``` Featureflip counts each event name, per environment. The `$metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### identify() Records an `Identify` event for the given user. The event is counted per environment, and the attributes you pass are not stored. This does not affect flag evaluation. [Targeting rules and segments](/docs/concepts/targeting/) are matched against the context you pass to each variation call. `identify()` does not set a context for subsequent calls, and it does not persist user attributes for later targeting. ```php $client->identify([ 'user_id' => 'u-123', 'email' => 'user@example.com', 'plan' => 'pro', ]); ``` identify([ 'user_id' => 'u-123', 'email' => 'user@example.com', 'plan' => 'pro',]);"> **Signature:** ```php public function identify(array $context): void ``` ## Lifecycle ### flush() Forces all buffered events to be sent to the server immediately. ```php $client->flush(); ``` flush();"> ### isInitialized() Returns whether the SDK holds a flag configuration, and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ```php if (!$client->isInitialized()) { // Nothing was ever loaded — every evaluation is returning the default you // pass. Usually a rejected SDK key, or an unreachable Evaluation API with // no cached snapshot to fall back on. } ``` isInitialized()) { // Nothing was ever loaded — every evaluation is returning the default you // pass. Usually a rejected SDK key, or an unreachable Evaluation API with // no cached snapshot to fall back on.}"> It answers “was anything ever loaded”, not “is it current”: a configuration retained through an outage still returns `true`, because the SDK is out of date rather than uninitialized. An environment with no flags in it also returns `true` — it loaded successfully, there was simply nothing to load. A closed handle always returns `false`. ### refresh() Refreshes the flag configuration if it is due one. Optional — evaluation refreshes by itself — but a persistent worker can call this from a tick hook so the fetch lands between requests instead of inside one: ```php Octane::tick('featureflip', fn () => $client->refresh())->seconds(10); ``` $client->refresh())->seconds(10);"> It honors `pollInterval` and the failure backoff, so calling it when nothing is due does no work. ### close() Releases this handle and, once the last one closes, flushes whatever events are still queued. A closed handle is inert: variation calls return the default you pass, `track()`/`identify()`/`flush()` do nothing, and no inspectors fire. Closing one handle never affects a sibling still holding the same core. Under PHP-FPM the SDK calls `fastcgi_finish_request()` first when there is a non-empty queue, so the response reaches the user before the analytics POST goes out. ```php $client->close(); ``` close();"> A shutdown function is registered automatically when the client is created, so `close()` is called when the process ends. You can call it explicitly if you need to flush events before the process exits. ## Testing ### FeatureflipClient::forTesting() Creates a test client with fixed flag values. No network calls are made and no PSR implementations are required. ```php $client = FeatureflipClient::forTesting([ 'dark-mode' => true, 'pricing-tier' => 'enterprise', 'rate-limit' => 500, ]); $client->boolVariation('dark-mode', [], false); // true $client->stringVariation('pricing-tier', [], 'free'); // "enterprise" ``` true, 'pricing-tier' => 'enterprise', 'rate-limit' => 500,]);$client->boolVariation('dark-mode', [], false); // true$client->stringVariation('pricing-tier', [], 'free'); // "enterprise""> **Signature:** ```php public static function forTesting(array $flags, array $inspectors = []): self ``` The client returns the override value for known flags, or the default value with a `FLAG_NOT_FOUND` reason for unknown flags. ## PSR Compatibility The SDK is designed to work with any PSR-compatible libraries. Common combinations: | Library | PSR-18 | PSR-17 | PSR-16 | | --- | --- | --- | --- | | Guzzle + Symfony Cache | GuzzleHttp\Client | GuzzleHttp\Psr7\HttpFactory | Symfony\Component\Cache\Psr16Cache | | Symfony HttpClient + Cache | Symfony\Component\HttpClient\Psr18Client | Nyholm\Psr7\Factory\Psr17Factory | Symfony\Component\Cache\Psr16Cache | | PHP-HTTP Discovery | Auto-discovered | Auto-discovered | Your choice | The SDK does not include any HTTP client or cache implementation to keep it lightweight and avoid dependency conflicts. ## See also - [PHP Quickstart](/docs/quickstart/php/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/python/ # Python SDK Reference **TL;DR:** Install `featureflip`, call `client.variation("flag-key", {"user_id": "u-123"}, default=False)`, and you’re evaluating flags — synchronous, no `asyncio`. The `featureflip` Python package provides a synchronous client for evaluating [feature flags](/docs/concepts/feature-flags/) in any Python 3.10+ application — Django, Flask, FastAPI, Celery workers, scripts, or notebooks. The SDK is intentionally synchronous: variation calls do not return coroutines, so you can drop them into existing sync codebases without rewriting call sites or introducing `asyncio` to a non-async project. Internally the client uses `httpx` for HTTP and SSE streaming, and stores flag configurations in a thread-safe in-memory cache. Evaluations read from this cache under a lock and return immediately, so they are safe to call from gunicorn workers, threaded WSGI servers, and Celery prefork pools alike. There is no GIL contention because the hot path is a dictionary lookup, not Python bytecode crunching JSON. Under a preforking server, build the client in the worker rather than the master (see Lifetime ). Evaluation context is a plain `dict[str, Any]` rather than a class — `{"user_id": "u-123", "plan": "pro"}` is the entire API. The `user_id` key is reserved: it anchors [percentage rollouts](/docs/concepts/rollout-strategies/) so a given user lands in a stable bucket across processes and pods. Custom keys feed into [targeting rules](/docs/concepts/targeting/) . String enum values from the API (rule operators, evaluation reasons) are normalized to lowercase on the client side, so you can compare them against Python literals without worrying about casing. New to Featureflip? Start with the [Python quickstart](/docs/quickstart/python/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-python) ## Installation ```bash pip install featureflip ``` The package depends on `httpx`, `httpx-sse`, and `structlog`. ## Lifetime The Featureflip Python SDK is designed to be used as a singleton across your application. Construct the client once and share it: ```python # In your application startup from featureflip import FeatureflipClient client = FeatureflipClient(sdk_key="your-sdk-key") ``` Calling `FeatureflipClient(sdk_key="x")` multiple times with the **same** SDK key returns distinct handle objects that share a single underlying client — the SDK maintains a process-wide cache keyed by SDK key. You cannot accidentally open duplicate streaming connections by constructing multiple clients. ```python # Both of these return distinct handle objects that share one underlying client c1 = FeatureflipClient(sdk_key="your-sdk-key") c2 = FeatureflipClient(sdk_key="your-sdk-key") assert c1 is not c2 # different handles # ...but only one HTTP connection, one polling thread, one event processor c1.close() # decrements refcount; c2 is still alive c2.close() # last handle closed; real shutdown runs ``` Different SDK keys get independent clients: ```python client_a = FeatureflipClient(sdk_key="sdk-key-a") client_b = FeatureflipClient(sdk_key="sdk-key-b") # Two independent clients ``` ### Use with Django, Flask, FastAPI Build the client inside each worker process, after the fork. The constructor starts background threads for the SSE stream and the analytics flush, and `os.fork()` copies only the calling thread. A client built in a preloading master hands every worker a flag store that no thread is updating, so evaluations keep returning whatever was true at fork time and nothing raises. The portable pattern defers construction to first use, which lands it in the worker under every server: myapp/flags.py ```python import os from featureflip import FeatureflipClient _client: FeatureflipClient | None = None def get_client() -> FeatureflipClient: global _client if _client is None: _client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) return _client ``` FeatureflipClient: global _client if _client is None: _client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) return _client"> Django’s `AppConfig.ready()` and a Flask `create_app()` factory both run per worker under a plain `gunicorn myproject.wsgi`, so calling `get_client()` from either is fine. Add `--preload` and the same code runs once in the master instead, and uWSGI preloads by default unless you set `lazy-apps = true`. Where you want construction pinned to the worker regardless of launch flags, use the server’s post-fork hook: Gunicorn’s `post_fork` in `gunicorn.conf.py`, uWSGI’s `@postfork` decorator, or a FastAPI `lifespan` context manager, which already runs per worker. The cache solves a different problem. Two `FeatureflipClient(...)` calls in one process share one connection, so duplicate construction is harmless. It cannot restart a thread the fork left behind. [Feature flags in Python](/blog/feature-flags-in-python/) works through the failure mode and each server’s hook in detail. ### Closing `close()` is refcounted. When multiple handles share one cached client, closing one handle does not shut down the shared background threads — the real shutdown runs only when the last handle is closed. In a typical application the client lives for the entire process lifetime and is cleaned up automatically at process exit or via a framework’s shutdown lifecycle. As a context manager: ```python with FeatureflipClient(sdk_key="your-sdk-key") as client: value = client.variation("flag", {"user_id": "123"}, default=False) # client.close() is called automatically; decrements refcount ``` ## Configuration The `Config` dataclass accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | base_url | str | "https://eval.featureflip.io" | Base URL of the Evaluation API | | connect_timeout | float | 5.0 | Connection timeout in seconds | | read_timeout | float | 10.0 | Read timeout in seconds | | streaming | bool | True | Enable SSE for real-time flag updates | | poll_interval | float | 30.0 | Polling interval in seconds (used when streaming is disabled) | | send_events | bool | True | Enable analytics event tracking | | flush_interval | float | 30.0 | Interval in seconds between event flush batches | | flush_batch_size | int | 100 | Maximum number of events per flush batch | | init_timeout | float | 10.0 | Maximum time in seconds to wait for initialization | ### Example configuration ```python from featureflip import Config config = Config( base_url="https://eval.featureflip.io", streaming=True, poll_interval=60.0, flush_interval=15.0, init_timeout=30.0, ) ``` All timeout and interval values must be positive. The constructor validates these constraints and raises `ValueError` for invalid values. ## Initialization Create a client by providing an SDK key and optional configuration: ```python from featureflip import FeatureflipClient, Config client = FeatureflipClient( sdk_key="sdk-dev-abc123", config=Config(base_url="https://eval.featureflip.io"), ) ``` The constructor performs initialization synchronously: it fetches the initial flag configuration, starts streaming or polling, and begins the event processor. If initialization fails or times out, an `InitializationError` is raised. ### SDK key resolution If `sdk_key` is not provided, the client falls back to the `FEATUREFLIP_SDK_KEY` environment variable. A `ValueError` is raised if neither is available. ```python import os os.environ["FEATUREFLIP_SDK_KEY"] = "sdk-dev-abc123" # SDK key read from environment client = FeatureflipClient() ``` ### Context manager The client supports Python’s context manager protocol for automatic cleanup: ```python with FeatureflipClient(sdk_key="sdk-dev-abc123") as client: enabled = client.variation("new-feature", {"user_id": "123"}, default=False) # client.close() is called automatically ``` ### is_initialized ```python client.is_initialized: bool ``` A read-only property that returns `True` once the client has successfully loaded flag data. ## Evaluating Flags The Python SDK uses a single `variation()` method for all flag types, rather than separate typed methods. ### variation() ```python enabled = client.variation("dark-mode", {"user_id": "u-123"}, default=False) plan = client.variation("pricing-tier", {"user_id": "u-123"}, default="free") limit = client.variation("rate-limit", {"user_id": "u-123"}, default=100) ``` **Signature:** ```python def variation( self, key: str, context: dict[str, Any], default: T, track: bool = True, ) -> T ``` T"> | Parameter | Type | Description | | --- | --- | --- | | key | str | The flag key to evaluate | | context | dict[str, Any] | User attributes for targeting. The user_id key is used for percentage rollouts | | default | T | Value returned if the flag is not found or evaluation fails | | track | bool | Whether to track this evaluation event (default True) | This method never raises exceptions. On any error, it logs a warning and returns the default value. ## Evaluation Details ### variation_detail() Returns an `EvaluationDetail` object with the evaluated value and metadata about the evaluation decision. ```python detail = client.variation_detail("dark-mode", {"user_id": "u-123"}, default=False) print(detail.value) # True print(detail.reason) # EvaluationReason.RULE_MATCH print(detail.rule_id) # "rule-abc" ``` **Signature:** ```python def variation_detail( self, key: str, context: dict[str, Any], default: T, ) -> EvaluationDetail ``` EvaluationDetail"> ### EvaluationDetail A frozen dataclass containing the evaluation result: | Property | Type | Description | | --- | --- | --- | | value | Any | The evaluated flag value | | reason | EvaluationReason | Why this value was returned | | rule_id | str | None | The ID of the matched targeting rule, if applicable | | error | Exception | None | The exception that occurred, if reason is ERROR | ### EvaluationReason An enum describing why a particular flag value was returned: | Value | Description | | --- | --- | | FALLTHROUGH | No rules matched; the fallthrough variation was served | | RULE_MATCH | A targeting rule matched the context | | FLAG_DISABLED | The flag is disabled; the off variation was served | | FLAG_NOT_FOUND | The flag key does not exist | | ERROR | An error occurred during evaluation | ## Event Tracking ### track() Records a custom analytics event. ```python client.track("purchase-completed", {"user_id": "u-123"}, metadata={ "plan": "pro", "amount": 29.99, }) ``` **Signature:** ```python def track( self, event_name: str, context: dict[str, Any], metadata: dict[str, Any] | None = None, ) -> None ``` None"> Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### identify() Records an `Identify` event for the given user. The event is counted per environment, and the attributes you pass are not stored. This does not affect flag evaluation. [Targeting rules and segments](/docs/concepts/targeting/) are matched against the context you pass to each variation call. `identify()` does not set a context for subsequent calls, and it does not persist user attributes for later targeting. ```python client.identify({ "user_id": "u-123", "email": "user@example.com", "plan": "pro", }) ``` **Signature:** ```python def identify(self, context: dict[str, Any]) -> None ``` None"> ## Flag Change Notifications ### on_update() Subscribes to flag-configuration updates. The listener receives the keys of the flags affected by each change, batched into one call however many flags it touched. ```python def on_flags_changed(keys: list[str]) -> None: print("flags changed:", keys) cache.invalidate(keys) unsubscribe = client.on_update(on_flags_changed) ``` None: print("flags changed:", keys) cache.invalidate(keys)unsubscribe = client.on_update(on_flags_changed)"> A flag is reported when it is created, deleted or redefined, when a [segment](/docs/concepts/targeting/) its targeting rules reference changes, and when a flag it depends on through a prerequisite changes. Those last two move a flag’s evaluated value without touching its own configuration. The listener does **not** fire for the initial flag load, and is never called with an empty list. Listeners run on the SDK’s streaming or polling thread, so they must not block: a slow listener delays flag delivery and, on the streaming path, can stall the connection. Do the minimum here and hand real work to a queue or thread pool. Exceptions raised by a listener are caught and logged. A throwing listener neither breaks flag delivery nor stops the remaining listeners. Returns a callable that unsubscribes. Listeners registered through a handle are also dropped when that handle is closed, so a handle that goes away cannot leave a listener firing off the shared core. **Signature:** ```python on_update(listener: Callable[[list[str]], None]) -> Callable[[], None] ``` Callable[[], None]"> ### off_update() Removes a listener previously registered with `on_update()`. Equivalent to calling the unsubscribe callable it returned. Removing a listener that is not registered is a no-op. ```python client.off_update(on_flags_changed) ``` ## Cleanup ### close() Shuts down the client, stops streaming or polling, flushes remaining events, and closes HTTP connections. ```python client.close() ``` ### flush() Forces all buffered events to be sent to the server immediately. ```python client.flush() ``` Using the context manager (`with` statement) is the recommended approach, as it calls `close()` automatically when the block exits. ## Testing ### FeatureflipClient.for_testing() Creates a test client with fixed flag values. No network calls are made and no background threads are started. ```python client = FeatureflipClient.for_testing({ "dark-mode": True, "pricing-tier": "enterprise", "rate-limit": 500, }) client.variation("dark-mode", {}, default=False) # True client.variation("pricing-tier", {}, default="free") # "enterprise" ``` **Signature:** ```python @classmethod def for_testing(cls, flags: dict[str, Any]) -> FeatureflipClient ``` FeatureflipClient"> ### set_test_value() Updates a flag value on a test client at runtime. Raises `RuntimeError` if called on a non-test client. ```python client = FeatureflipClient.for_testing({"dark-mode": True}) # Change the value during the test client.set_test_value("dark-mode", False) client.variation("dark-mode", {}, default=True) # False ``` **Signature:** ```python def set_test_value(self, key: str, value: Any) -> None ``` None"> ## Types ### Public exports All public types are available from the top-level `featureflip` package: ```python from featureflip import ( FeatureflipClient, Config, EvaluationContext, EvaluationDetail, EvaluationReason, FlagUpdateListener, InitializationError, ConfigurationError, FeatureflipError, ) ``` ### Exception hierarchy | Exception | Description | | --- | --- | | FeatureflipError | Base exception for all SDK errors | | InitializationError | Raised when initialization fails or times out | | ConfigurationError | Raised for invalid configuration values | ## See also - [Python Quickstart](/docs/quickstart/python/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [OpenFeature](/docs/integrations/openfeature/) — use Featureflip through the vendor-neutral OpenFeature standard - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/react/ # React SDK Reference **TL;DR:** Install `@featureflip/react`, wrap your tree in ``, call `useFeatureFlag('flag-key', false)` in components — re-renders automatically. The `@featureflip/react` package wraps the [Browser SDK](/docs/sdks/browser/) in idiomatic React: a `` at the root of your tree, and a `useFlag` hook in any component that needs to read [feature flag](/docs/concepts/feature-flags/) values. Components re-render automatically when a flag’s value changes — no manual subscriptions, no global event bus, no `forceUpdate` plumbing. The package supports both React 18 and React 19 and works with the React Compiler, Server Components, and Suspense. Internally, the hooks use React 18’s `useSyncExternalStore` so that flag reads are concurrent-mode safe and tearing-free. Snapshot identity is stable across re-renders (memoized via a `useRef` cache keyed on a JSON serialization), so a flag returning the same value won’t trigger spurious re-renders even when other flags update around it. The provider is hardened against React StrictMode’s intentional double-mount/double-effect behavior: the underlying Browser SDK (v2.0+) is **singleton-by-construction** via `FeatureflipClient.get(clientKey, ...)`, so StrictMode’s simulated unmount/remount cycle returns handles sharing the same underlying core — the refcount stays positive across the cycle and the SSE connection is never actually torn down. Multiple `` instances with the same `clientKey` (for example, in a micro-frontend architecture) likewise share one connection. The provider’s `identify()` method updates the active context (e.g., after sign-in) and the new flag values flow through `useFlag` automatically. Errors are surfaced via the provider’s `'error'` event so you can show fallback UI without try/catching every component. This is the recommended SDK for any React app — Next.js, Remix, Vite, CRA, React Native Web — that needs in-app [percentage rollouts](/docs/concepts/rollout-strategies/) and [targeting rules](/docs/concepts/targeting/) reacting to user state in real time. New to Featureflip? Start with the [JavaScript quickstart](/docs/quickstart/javascript/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-react) ## Requirements - React 18.0+ or 19.0+ - Node.js 20.19.0+ (for build tooling) ## Installation ```bash npm install @featureflip/react @featureflip/browser ``` ## Quick Start Wrap your app with `FeatureflipProvider`, then use `useFeatureFlag` in any component. ```tsx import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react'; function App() { return ( ); } function Banner() { const showBanner = useFeatureFlag('show-banner', false); if (!showBanner) return null; return
New feature available!
; } ```    );}function Banner() { const showBanner = useFeatureFlag('show-banner', false); if (!showBanner) return null; return New feature available! ;}"> ## Provider ### FeatureflipProvider Wraps your component tree and manages the `FeatureflipClient` lifecycle. Creates the client on mount, calls `initialize()`, and cleans up on unmount. | Prop | Type | Default | Description | | --- | --- | --- | --- | | clientKey | string | (required) | Client SDK key | | context | Record | undefined | Evaluation context (user attributes) | | baseUrl | string | undefined | Evaluation API base URL | | streaming | boolean | undefined | Enable SSE streaming | When the `context` prop changes, the provider automatically calls `identify()` and re-evaluates all flags. ## Hooks ### useFeatureFlag(key, defaultValue): T Returns the evaluated value of a flag. The component re-renders automatically when the flag value changes via streaming or `identify()`. The type of `defaultValue` determines which variation method is called internally. ```tsx const enabled = useFeatureFlag('new-checkout', false); // boolean const color = useFeatureFlag('button-color', 'blue'); // string const limit = useFeatureFlag('rate-limit', 100); // number const config = useFeatureFlag('ui-config', { sidebar: true }); // object ``` ### useFeatureflipStatus(): { isReady, isError, error } Returns the initialization status of the client. Useful for showing loading states. ```tsx function StatusGate({ children }: { children: React.ReactNode }) { const { isReady, isError, error } = useFeatureflipStatus(); if (isError) return
Error: {error?.message}
; if (!isReady) return
Loading...
; return <>{children}; } // Use inside a FeatureflipProvider
``` Error: {error?.message} ; if (!isReady) return Loading... ; return <>{children};}// Use inside a FeatureflipProvider   "> ### useFeatureflipClient(): FeatureflipClient Returns the underlying `FeatureflipClient` instance for direct access — for example, calling `identify()` after login. ## Identifying Users You can identify users in two ways: **Via context prop** — the provider calls `identify()` automatically when the prop changes: ```tsx import { useState } from 'react'; function App() { const [user, setUser] = useState(null); return (
); } ```    );}"> **Via the client directly:** ```tsx function LoginPage() { const client = useFeatureflipClient(); async function handleLogin(userId: string) { // ... authenticate ... await client.identify({ user_id: userId, plan: 'pro' }); } return ; } ``` handleLogin('123')}>Log in;}"> ## Testing Use `TestFeatureflipProvider` to supply predetermined flag values in tests — no network calls, no initialization delay. ```tsx import { TestFeatureflipProvider } from '@featureflip/react'; import { render, screen } from '@testing-library/react'; test('renders banner when flag is on', () => { render( , ); expect(screen.getByText('New feature available!')).toBeInTheDocument(); }); ``` { render(   , ); expect(screen.getByText('New feature available!')).toBeInTheDocument();});"> `TestFeatureflipProvider` uses `FeatureflipClient.forTesting()` internally and sets `isReady: true` immediately. ## See also - [JavaScript / TypeScript SDK](/docs/sdks/browser/) — framework-agnostic browser SDK (used internally by React SDK) - [Server-side evaluation in Next.js](/blog/feature-flags-in-nextjs/) — pair this provider with the Node SDK so the first client render matches the server HTML - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/ruby/ # Ruby SDK Reference **TL;DR:** Run `gem install featureflip`, call `client.bool_variation("flag-key", { user_id: "u-123" }, false)`, and you’re evaluating flags — zero runtime dependencies. The `featureflip` Ruby gem provides a thread-safe client for evaluating [feature flags](/docs/concepts/feature-flags/) in any Ruby 3.2+ application — Rails, Sinatra, Hanami, Sidekiq workers, Rake tasks, or plain scripts. The SDK has zero runtime gem dependencies; it uses the standard library’s `net/http` for HTTPS and SSE streaming, so it doesn’t fight with your existing HTTP gem stack and won’t pull in `faraday` or `httparty` transitively. Distribution is RubyGems under the `featureflip` name. The client is intended as a long-lived process-level singleton. In Rails, instantiate it once in an initializer and store it on a class constant or `Rails.configuration`; in Sidekiq, share it across workers; in Puma, build it after fork. Variation methods read from a `Concurrent::Hash` and return synchronously, so you can call them from controllers, background jobs, mailers, or partials without thread-safety concerns. The streaming connection runs on a background `Thread`, which gracefully handles GVL contention because the hot path is simple I/O and a hash lookup. Evaluation context is a plain Ruby `Hash` with a required `user_id` key — `{ user_id: "u-123", plan: "pro", country: "US" }` is the entire API. The `user_id` anchors [percentage rollouts](/docs/concepts/rollout-strategies/) so a given user lands in a stable bucket across processes, dynos, and pods. Custom keys feed [targeting rules](/docs/concepts/targeting/) . Symbol and string keys both work — the SDK normalizes them internally — so you don’t have to fight the hash-keys-as-strings-or-symbols ambiguity that plagues most Ruby APIs. New to Featureflip? Start with the [Ruby quickstart](/docs/quickstart/ruby/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-ruby) ## Installation Add to your Gemfile: ```ruby gem "featureflip" ``` Or install directly: ```bash gem install featureflip ``` The gem has zero runtime dependencies — it uses only Ruby stdlib (`net/http`, `digest/md5`). ## Configuration The `Config` class accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | base_url | String | "https://eval.featureflip.io" | Base URL of the Evaluation API | | connect_timeout | Numeric | 5 | Connection timeout in seconds | | read_timeout | Numeric | 10 | Read timeout in seconds | | streaming | Boolean | true | Enable SSE for real-time flag updates | | poll_interval | Numeric | 30 | Polling interval in seconds (used when streaming is disabled) | | send_events | Boolean | true | Enable analytics event tracking | | flush_interval | Numeric | 30 | Interval in seconds between event flush batches | | flush_batch_size | Integer | 100 | Maximum number of events per flush batch | | init_timeout | Numeric | 10 | Maximum time in seconds to wait for initialization | | max_stream_retries | Integer | 5 | SSE retries before falling back to polling | ### Example configuration ```ruby config = Featureflip::Config.new( base_url: "https://eval.featureflip.io", streaming: true, poll_interval: 60, flush_interval: 15, init_timeout: 30, ) ``` All timeout and interval values must be positive. The constructor validates these constraints and raises `Featureflip::ConfigurationError` for invalid values. ## Initialization ### Instance-based Create a client by providing an SDK key and optional configuration: ```ruby client = Featureflip::Client.new( sdk_key: "sdk-dev-abc123", config: Featureflip::Config.new(base_url: "https://eval.featureflip.io"), ) ``` The constructor performs initialization synchronously: it fetches the initial flag configuration, starts streaming or polling, and begins the event processor. If initialization fails or times out, an `InitializationError` is raised. ### Singleton convenience API For applications that use a single client instance, the `Featureflip` module provides a convenience wrapper: ```ruby Featureflip.configure do |c| c.sdk_key = "sdk-dev-abc123" c.base_url = "https://eval.featureflip.io" c.streaming = true end enabled = Featureflip.bool_variation("dark-mode", { user_id: "u-123" }, false) Featureflip.close ``` The singleton is thread-safe — `configure` and `close` are protected by a `Mutex`. ### SDK key resolution If `sdk_key` is not provided, the client falls back to the `FEATUREFLIP_SDK_KEY` environment variable. A `ConfigurationError` is raised if neither is available. ```ruby ENV["FEATUREFLIP_SDK_KEY"] = "sdk-dev-abc123" # SDK key read from environment client = Featureflip::Client.new ``` ### initialized? ```ruby client.initialized? # => true ``` true"> Returns `true` once the client has successfully loaded flag data, and `false` after `close` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags The Ruby SDK provides separate typed methods for each flag type. Context can use string or symbol keys — symbols are automatically converted to strings. ### bool_variation ```ruby enabled = client.bool_variation("dark-mode", { user_id: "u-123" }, false) ``` ### string_variation ```ruby tier = client.string_variation("pricing-tier", { user_id: "u-123" }, "free") ``` ### number_variation ```ruby limit = client.number_variation("rate-limit", { user_id: "u-123" }, 100) ``` ### json_variation ```ruby config = client.json_variation("ui-config", { user_id: "u-123" }, { "theme" => "light" }) ``` "light" })"> **Signature** (same for all typed methods): ```ruby def bool_variation(key, context, default_value) ``` | Parameter | Type | Description | | --- | --- | --- | | key | String | The flag key to evaluate | | context | Hash | User attributes for targeting. The user_id key is used for percentage rollouts | | default_value | Object | Value returned if the flag is not found or evaluation fails | These methods never raise exceptions. On any error, the default value is returned. ## Evaluation Details ### variation_detail Returns an `EvaluationDetail` with the evaluated value and metadata about the evaluation decision. ```ruby detail = client.variation_detail("dark-mode", { user_id: "u-123" }, false) detail.value # true detail.reason # "RuleMatch" detail.rule_id # "rule-abc" detail.variation_key # "true" ``` **Signature:** ```ruby def variation_detail(key, context, default_value) ``` ### EvaluationDetail A `Struct` containing the evaluation result: | Property | Type | Description | | --- | --- | --- | | value | Object | The evaluated flag value | | reason | String | Why this value was returned | | rule_id | String or nil | The ID of the matched targeting rule, if applicable | | variation_key | String or nil | The key of the matched variation | ### Evaluation reasons | Value | Description | | --- | --- | | "Fallthrough" | No rules matched; the fallthrough variation was served | | "RuleMatch" | A targeting rule matched the context | | "FlagDisabled" | The flag is disabled; the off variation was served | | "FlagNotFound" | The flag key does not exist | | "Error" | An error occurred during evaluation | ## Event Tracking ### track Records a custom analytics event. ```ruby client.track("purchase-completed", { user_id: "u-123" }, { plan: "pro", amount: 29.99, }) ``` **Signature:** ```ruby def track(event_key, context, metadata = nil) ``` Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### identify Records an `Identify` event for the given user. The event is counted per environment, and the attributes you pass are not stored. This does not affect flag evaluation. [Targeting rules and segments](/docs/concepts/targeting/) are matched against the context you pass to each variation call. `identify()` does not set a context for subsequent calls, and it does not persist user attributes for later targeting. ```ruby client.identify({ user_id: "u-123", email: "user@example.com", plan: "pro", }) ``` **Signature:** ```ruby def identify(context) ``` ## Cleanup ### close Shuts down the client, stops streaming or polling, and flushes remaining events. ```ruby client.close ``` Can be called multiple times safely. ### flush Forces all buffered events to be sent to the server immediately. ```ruby client.flush ``` ### restart Respawns background threads after a fork. Required for Puma, Unicorn, and Passenger. ```ruby client.restart ``` ## Fork Handling Ruby web servers that use forking (Puma, Unicorn, Passenger) kill background threads in worker processes. Call `restart` after fork to respawn streaming/polling threads: config/puma.rb ```ruby on_worker_boot do Featureflip.restart end # config/unicorn.rb after_fork do |server, worker| Featureflip.restart end ``` ## Testing ### Client.for_testing Creates a test client with fixed flag values. No network calls are made and no background threads are started. ```ruby client = Featureflip::Client.for_testing( "dark-mode" => true, "pricing-tier" => "enterprise", "rate-limit" => 500, ) client.bool_variation("dark-mode", {}, false) # true client.string_variation("pricing-tier", {}, "free") # "enterprise" client.bool_variation("unknown", {}, false) # false (default) ``` true, "pricing-tier" => "enterprise", "rate-limit" => 500,)client.bool_variation("dark-mode", {}, false) # trueclient.string_variation("pricing-tier", {}, "free") # "enterprise"client.bool_variation("unknown", {}, false) # false (default)"> **Signature:** ```ruby def self.for_testing(flags) ``` | Parameter | Type | Description | | --- | --- | --- | | flags | Hash | A hash mapping flag keys to their values | ### Using with RSpec ```ruby RSpec.describe MyService do let(:client) { Featureflip::Client.for_testing("premium-feature" => true) } it "enables premium feature" do service = MyService.new(feature_client: client) expect(service.premium_enabled?).to be true end end ``` true) } it "enables premium feature" do service = MyService.new(feature_client: client) expect(service.premium_enabled?).to be true endend"> ## Types ### Public classes All public types are available from the `Featureflip` namespace: ```ruby require "featureflip" Featureflip::Client Featureflip::Config Featureflip::Models::EvaluationDetail ``` ### Exception hierarchy | Exception | Description | | --- | --- | | Featureflip::Error | Base exception for all SDK errors | | Featureflip::InitializationError | Raised when initialization fails or times out | | Featureflip::ConfigurationError | Raised for invalid configuration values | ## See also - [Ruby Quickstart](/docs/quickstart/ruby/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Rollout Strategies](/docs/concepts/rollout-strategies/) — gradual percentage-based rollouts - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs --- ## Source: https://featureflip.io/docs/sdks/swift/ # Swift SDK Reference **TL;DR:** Add the `featureflip-swift` Swift Package, call `client.boolVariation("flag-key", default: false)`, and you’re evaluating flags from any iOS or macOS app. The `Featureflip` Swift package provides a client-side SDK for evaluating [feature flags](/docs/concepts/feature-flags/) in Apple platform apps. It fetches evaluated flag values from the Featureflip Evaluation API, supports real-time updates via SSE streaming, disk caching for offline starts, and automatic lifecycle management (pausing on background, resuming on foreground). New to Featureflip? Start with the [Swift quickstart](/docs/quickstart/swift/) — it walks through installation, initializing the client, and evaluating your first flag in under 5 minutes. [View source on GitHub](https://github.com/canopy-labs/featureflip-swift) ## Requirements - Swift 5.9+ | Platform | Minimum Version | | --- | --- | | iOS | 15.0 | | macOS | 13.0 | | tvOS | 15.0 | | watchOS | 8.0 | ## Installation Add the package via Swift Package Manager. In Xcode: **File → Add Package Dependencies**, then enter the repository URL. Or in `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/canopy-labs/featureflip-swift", from: "2.0.0") ] ``` ## Configuration The `FeatureflipConfig` struct accepts the following options: | Option | Type | Default | Description | | --- | --- | --- | --- | | clientKey | String | required | Client SDK key from your project settings | | baseUrl | String | "https://eval.featureflip.io" | Evaluation API base URL | | context | [String: Any] | [:] | Initial evaluation context (user attributes). Values keep their JSON type — a number stays a number, so numeric equality rules target correctly | | streaming | Bool | true | Enable SSE for real-time flag updates | | pollInterval | TimeInterval | 30 | Polling interval in seconds (used when streaming is disabled) | | flushInterval | TimeInterval | 30 | Interval in seconds between event flush batches | | flushBatchSize | Int | 100 | Maximum number of events per flush batch | | initTimeout | TimeInterval | 10 | Maximum time in seconds to wait for initial flag fetch | ### Example configuration ```swift let config = FeatureflipConfig( clientKey: "your-client-sdk-key", context: ["user_id": "u-123"], streaming: true, pollInterval: 60, initTimeout: 15 ) ``` ## Initialization ### Instance-based Create a client and call `initialize()`: ```swift import Featureflip let config = FeatureflipConfig(clientKey: "your-client-sdk-key") let client = FeatureflipClient(config: config) await client.initialize() ``` `initialize()` loads any cached flags from disk, fetches fresh flag values from the API, starts SSE streaming (or polling), and begins the event processor. The SDK is usable with cached values even if the network request fails. ### isInitialized ```swift client.isInitialized: Bool ``` A read-only property that returns `true` once the client has successfully loaded flag data, and `false` after `close()` — a closed handle serves the caller’s default from every accessor. ## Evaluating Flags All variation methods are synchronous and return immediately from an in-memory snapshot. They accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type. Since this is a client-side SDK, evaluation context is set at initialization or via `identify()` — it is not passed per evaluation call. ### boolVariation(_:default:) ```swift let enabled = client.boolVariation("dark-mode", default: false) ``` ### stringVariation(_:default:) ```swift let color = client.stringVariation("button-color", default: "blue") ``` ### numberVariation(_:default:) ```swift let limit = client.numberVariation("rate-limit", default: 100.0) ``` Returns a `Double`. ### jsonVariation(_:default:) ```swift let config = client.jsonVariation("ui-config", default: .dictionary(["theme": .string("light")])) ``` Returns an `AnyCodableValue` enum (see Types ). ## Identifying Users Call `identify()` to re-evaluate all flags for a new user context — typically after login. This makes a network request to the evaluation API with the new context and updates all cached flag values, applying [targeting rules](/docs/concepts/targeting/) and [percentage rollouts](/docs/concepts/rollout-strategies/) to the new attributes. ```swift try await client.identify(context: ["user_id": "u-123", "plan": "pro"]) ``` The streaming or polling data source is also updated with the new context. ## Event Tracking ### track(_:metadata:) Enqueues a custom analytics event. Events are batched and flushed automatically. ```swift client.track("purchase-completed", metadata: [ "plan": .string("pro"), "amount": .double(29.99), ]) ``` Featureflip counts each event name, per environment. The `metadata` you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API. ### flush() Forces all buffered events to be sent immediately. ```swift await client.flush() ``` ## Cleanup ### close() Stops streaming or polling and flushes remaining events. ```swift await client.close() ``` ### Lifecycle management The SDK automatically observes app lifecycle events: - **Background**: Stops streaming/polling and flushes events - **Foreground**: Resumes streaming/polling No manual intervention is required. ## Lifetime ### Shared core Two `FeatureflipClient(config:)` calls with the same `clientKey` return distinct handle objects that share one underlying core. The core holds all expensive resources (network connections, caches, event processor), while each handle is a lightweight wrapper. ```swift let a = FeatureflipClient(config: config) let b = FeatureflipClient(config: config) // a and b share the same underlying core ``` ### Refcounted close() Each `close()` call decrements the core’s reference count. The core only shuts down (stops streaming, flushes events) when the last handle calls `close()`. ```swift await a.close() // core stays alive (b still open) await b.close() // core shuts down ``` ### Idempotent initialize() `initialize()` is idempotent on the shared core. The first call performs the real work (disk cache load, network fetch, start streaming). Subsequent calls from any handle return immediately. ### SwiftUI Each handle has its own `FeatureFlagProvider` instance. `FeatureFlagProvider` works per-handle — inject the handle’s `flagProvider` into the SwiftUI environment as usual. ## SwiftUI Integration The SDK provides a `FeatureFlagProvider` for reactive SwiftUI updates. Access it via the client’s `flagProvider` property. ```swift struct ContentView: View { @ObservedObject var flags: FeatureFlagProvider init(client: FeatureflipClient) { self.flags = client.flagProvider } var body: some View { if flags.boolVariation("show-banner", default: false) { Text("New feature available!") } } } ``` The provider is an `ObservableObject` that triggers view updates when flag values change via streaming or `identify()`. ## Testing ### FeatureflipClient.forTesting(_:) Creates a test client with static flag overrides. No network calls are made and no background tasks are started. The client is immediately initialized. ```swift let client = FeatureflipClient.forTesting([ "dark-mode": true, "pricing-tier": "enterprise", "rate-limit": 500, ]) client.boolVariation("dark-mode", default: false) // true client.stringVariation("pricing-tier", default: "free") // "enterprise" client.boolVariation("unknown", default: false) // false (default) ``` **Signature:** ```swift static func forTesting(_ overrides: [String: Any]) -> FeatureflipClient ``` FeatureflipClient"> Supported override value types: `Bool`, `String`, `Int`, `Double`. ## Types ### AnyCodableValue A type-erased enum used for JSON flag values and event metadata: ```swift public enum AnyCodableValue: Sendable, Equatable, Codable { case bool(Bool) case string(String) case int(Int) case double(Double) case dictionary([String: AnyCodableValue]) case array([AnyCodableValue]) case null } ``` ### FlagValue The internal representation of a flag returned by the server: | Property | Type | Description | | --- | --- | --- | | value | AnyCodableValue | The evaluated flag value | | variation | String | The key of the matched variation | | reason | String | Why this value was returned | ### Flag reasons | Value | Description | | --- | --- | | "Fallthrough" | No rules matched; the fallthrough variation was served | | "RuleMatch" | A targeting rule matched the context | | "FlagDisabled" | The flag is disabled; the off variation was served | | "FlagNotFound" | The flag key does not exist | | "Error" | An error occurred during evaluation | | "TEST" | Returned by test clients | ## Platforms | Platform | Minimum Version | | --- | --- | | iOS | 15.0 | | macOS | 13.0 | | tvOS | 15.0 | | watchOS | 8.0 | ## See also - [Swift Quickstart](/docs/quickstart/swift/) — get started in under 5 minutes - [Targeting & Segments](/docs/concepts/targeting/) — control which users see which variation - [Evaluation API](/docs/api/overview/) — REST API reference - [SDK Overview](/docs/sdks/overview/) — compare server-side and client-side SDKs