
Feature Flags Architect
- 414 installs
- 23.8k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
feature-flags-architect is a release engineering skill that manages feature-flag lifecycles, rollouts, and debt audits for developers shipping progressive delivery safely.
About
Architects enterprise-grade feature-flag systems for SaaS and API products, defining flag types, evaluation flows, targeting rules, rollout strategies, and operational patterns that separate deployment from user-facing release control.
- Flag taxonomy and naming
- Rollout and kill-switch design
- Environment targeting rules
- SDK and evaluation patterns
- Release decoupling strategy
Feature Flags Architect by the numbers
- 414 all-time installs (skills.sh)
- +6 installs in the week ending Jun 23, 2026 (Skillselion tracking)
- Ranked #58 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill feature-flags-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 414 |
|---|---|
| repo stars | ★ 23.8k |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you audit stale feature flags in a repo?
Design a feature-flag architecture for gradual rollouts, kill switches, environment targeting, and safe experimentation across services and client apps.
Who is it for?
Release engineers and backend developers managing progressive delivery who need rollout plans, debt scans, and kill-switch audits without extra pip dependencies.
Skip if: Teams wanting hosted flag infrastructure provisioning alone without lifecycle discipline, scripts, or documentation audits.
When should I use this skill?
User mentions feature flags, rollout plan, kill switch, flag debt, LaunchDarkly, GrowthBook, Statsig, Unleash, Flipt, or progressive delivery.
What you get
Flag debt JSON reports, phased rollout schedules, kill-switch audit results, and taxonomy-aligned cleanup plans.
- flag debt scan report
- phased rollout schedule
- kill-switch audit JSON
By the numbers
- Version 2.9.0 with 3 stdlib Python CLI tools and 4 reference documents
- Defines 4 feature-flag types with distinct lifecycle and ownership rules
- Rollout planner supports 4 strategies: ring, linear, log, and cohort
Files
Feature Flags Architect
End-to-end discipline for feature flags: classify them, ship them, ramp them, and retire them. Most teams treat flags as throwaway if-statements; this skill treats them as a controlled lifecycle with measurable debt.
When to use
- Adding a new flag and need a rollout plan
- Auditing a codebase for stale or orphaned flags
- Choosing a flag provider (LaunchDarkly vs GrowthBook vs Statsig vs Unleash vs Flipt vs build-your-own)
- Designing a kill-switch path for a risky launch
- Cleaning up flag debt before a release freeze
- Reviewing whether a feature should ship behind a flag at all
Core principle: flags are a lifecycle, not an if
request → design → ship → ramp → cleanup → archiveFlags that skip cleanup become debt: dead branches, stale defaults, untested code paths, unbounded blast radius. The three scripts in this skill enforce the lifecycle.
Quick start
# 1. Audit the repo for flag debt
python scripts/flag_debt_scanner.py --repo . --max-age-days 90
# 2. Plan a progressive rollout for a new flag
python scripts/rollout_planner.py --population 100000 --target-percent 100 --duration-days 14 --strategy ring
# 3. Verify every flag has a documented kill switch
python scripts/kill_switch_audit.py --repo . --flag-doc docs/feature-flags.mdThe 4 flag types (taxonomy)
Different flag types have different lifespans and ownership. Misclassifying creates debt.
| Type | Purpose | Typical lifespan | Owner | Cleanup trigger |
|---|---|---|---|---|
| Release | Hide unfinished features in production | days–weeks | Eng | 100% rollout reached |
| Experiment | A/B test variants | weeks | Product/Marketing | Test concluded; winner picked |
| Operational | Circuit breakers, perf toggles, kill switches | months–years | Eng/SRE | Replaced by autoscaling/feature retirement |
| Permission | Entitlements per user/account/plan | years (permanent) | Product | Plan/role removed |
Only Release and Experiment flags should be on a debt-scanner watchlist. Operational and Permission flags are by design long-lived. See references/flag_taxonomy.md for decision tree.
The 3 Python tools
All three are stdlib-only. Run with --help.
flag_debt_scanner.py
Finds flags older than --max-age-days with low usage, suggesting candidates for cleanup.
python scripts/flag_debt_scanner.py --repo . --max-age-days 90 --format text
python scripts/flag_debt_scanner.py --repo . --max-age-days 60 --format json > debt.jsonDetection heuristic: 1. Walk --repo for code references matching common flag-call patterns:
flag("..."),isFlagEnabled("..."),featureFlag("..."),getFlag("...")client.variation("...", ...),unleash.isEnabled("..."),growthbook.feature("...")
2. For each unique flag identifier, find the oldest commit that introduced it (git log --diff-filter=A -S <name>). 3. Flag as DEBT if introduced > --max-age-days ago AND used in ≤--min-uses places.
Outputs flag name, age in days, file references, suggested action. JSON mode is CI-friendly.
rollout_planner.py
Generates a phased rollout schedule from population size, target percent, duration, and strategy.
python scripts/rollout_planner.py --population 100000 --target-percent 100 --duration-days 14 --strategy ring
python scripts/rollout_planner.py --population 50000 --target-percent 25 --duration-days 7 --strategy linear
python scripts/rollout_planner.py --population 1000000 --target-percent 100 --duration-days 30 --strategy logStrategies:
ring: 1% → 5% → 25% → 50% → 100%, evenly spaced. Default for risky launches.linear: constant rate per day. Default for medium-risk.log: rapid early, slow tail. Default for low-risk launches with confidence.cohort: by named cohort (internal → beta → free → paid → all).
Outputs a markdown table with date, percent, expected user count, abort criteria, and verification step per phase.
kill_switch_audit.py
Cross-references code-discovered flags against documentation to verify each has a kill switch path written down.
python scripts/kill_switch_audit.py --repo . --flag-doc docs/feature-flags.md
python scripts/kill_switch_audit.py --repo . --flag-doc runbooks/flags.md --format jsonWhat it checks: 1. Every code-discovered flag has an entry in --flag-doc 2. Each entry declares: owner, type, kill-switch trigger, monitoring dashboard 3. Reports flags missing documentation (FAIL) or missing fields (WARN)
Use as a pre-merge gate before any new flag ships.
Provider chooser (5 + DIY)
| Provider | Best for | Pricing model | Lock-in risk | OSS option |
|---|---|---|---|---|
| LaunchDarkly | Enterprise, complex targeting, audit/compliance | Per-MAU, expensive | High | No |
| GrowthBook | Mid-market, A/B testing focused, OSS-friendly | Per-MAU + OSS | Low | Yes (self-host) |
| Statsig | Growth/product teams, advanced experimentation | Free tier + per-MAU | Medium | No |
| Unleash | OSS-first, self-hosted, dev-friendly | OSS + Enterprise | Low | Yes |
| Flipt | Lightweight, k8s-native, simple needs | OSS-only | None | Yes |
| DIY | <100 flags, no targeting, full control | None | None | N/A |
Decision rules:
- <50 flags + no targeting → DIY with config file or env vars
- Need analytics + experimentation → Statsig or GrowthBook
- Compliance/SOC2 audit logs required → LaunchDarkly
- Self-hosting required (data residency / air-gapped) → Unleash or Flipt
- See
references/provider_comparison.mdfor detail.
Workflows
Workflow 1: Ship a new feature behind a flag
1. Classify: which of the 4 flag types?
→ Release (most common for engineering work)
2. Run rollout_planner.py to design the ramp
3. Add flag entry to docs/feature-flags.md BEFORE writing code:
- name, owner, type, kill-switch trigger, dashboard URL
4. Write the code with the flag
5. Run kill_switch_audit.py — must pass before merge
6. Deploy at 0%; verify kill switch works
7. Execute rollout schedule; abort if abort criteria met
8. At 100% for 7+ days: remove flag, delete dead branch, archive doc entryWorkflow 2: Quarterly flag cleanup
1. Run flag_debt_scanner.py --repo . --max-age-days 90 > debt.md
2. For each flagged item:
a. Confirm it reached 100% (or was killed)
b. Find the issue/PR that introduced it; verify owner agrees to remove
c. Delete dead branches; remove flag config
d. Run kill_switch_audit.py — should now show one fewer flag
3. Update CHANGELOG: "Removed N stale flags"Workflow 3: Choose a provider
1. Estimate flag count (current + 12-month projection)
2. Required features:
- Targeting rules (user, account, geo, %)?
- A/B testing + stats?
- Audit log / SOC2?
- Self-hosting / data residency?
3. Pricing budget (MAU * cost-per-MAU)
4. See provider_comparison.md decision tree
5. Build a 30-day proof-of-concept before signingWorkflow 4: Design a kill switch
1. Identify the failure modes:
- Latency spike (which threshold?)
- Error rate spike (which threshold?)
- Business metric regression (which threshold?)
2. Wire each to an abort:
- Manual: dashboard link + on-call playbook
- Automated: alert threshold flips flag back to 0%
3. Test the kill switch in staging BEFORE production rollout
4. Document in flag-doc; pass kill_switch_audit.pyReferences
references/flag_taxonomy.md— 4 types, decision tree, ownership, lifespanreferences/provider_comparison.md— LaunchDarkly / GrowthBook / Statsig / Unleash / Flipt / DIY trade-offsreferences/rollout_strategies.md— ring / linear / log / cohort / geo, abort criteria, monitoringreferences/flag_lifecycle.md— request → design → ship → ramp → cleanup → archive
Slash command
/flag-cleanup — Run the full cleanup workflow on the current repo: scan for debt, generate a removal plan, audit kill switches.
Asset templates
assets/flag_request_template.md— fill-in form for new flag requests (name, owner, type, kill switch, rollout plan)
Anti-patterns
- Permanent flag with `if (FLAG_FOO)` 50 places — should be a Permission flag with a runtime config, not a Release flag
- Flag with no owner — when the original engineer leaves, no one cleans it up
- No kill switch documented — when the feature breaks, no one knows how to disable it
- A/B test that ran 6 months — pick a winner; running indefinitely is debt
- Flags as feature toggles for cosmetic changes — ship via deploy, not flag
Verifiable success
A team using this skill should achieve:
- 100% of new flags pass
kill_switch_audit.pyat merge time flag_debt_scanner.py --max-age-days 90returns ≤5 stale flags repo-wide- Every flag has a documented owner, type, and kill switch
- Mean time to retire a Release flag: <60 days from 100% rollout
Feature flag request
Fill in every section before opening a PR that adds the flag.
Basics
- Name:
<kebab-case-flag-name>(e.g.,new-checkout-flow) - Owner:
<your-handle@team> - Type: [ ] Release [ ] Experiment [ ] Operational [ ] Permission
- Created:
<YYYY-MM-DD> - Expected cleanup:
<YYYY-MM-DD or "permanent">
Justification
Why a flag and not a direct deploy?
(Examples: risky launch, A/B test, kill-switch needed, gradual rollout, compliance requirement)
Rollout plan
Generated by rollout_planner.py. Paste output below.<paste output here>Kill switch
- Trigger:
<concrete signal that flips the flag back to 0%> - Threshold:
<numeric threshold> - Method: [ ] Manual via dashboard URL [ ] Automated via alert webhook
- Runbook:
<link to on-call runbook>
Monitoring
- Dashboard:
<URL> - Key metrics to watch:
<metric 1>baseline:<value>, abort threshold:<value><metric 2>baseline:<value>, abort threshold:<value>
Code locations
- Decision point:
<file:line>(single point of conditional) - Provider used:
<LaunchDarkly | GrowthBook | Statsig | Unleash | Flipt | DIY> - SDK:
<sdk version / config file path>
Tests
- [ ] Test for ON branch
- [ ] Test for OFF branch
- [ ] Kill-switch test in staging (verify flag flip works)
Cleanup criteria
When can this flag be removed?
(Example: at 100% rollout for ≥7 days with no incidents)
Pre-merge checklist
- [ ]
kill_switch_audit.pypasses - [ ] flag-doc entry added with all required fields
- [ ] PR description links to this template
- [ ] Owner has write access to the provider dashboard
- [ ] Abort criteria are concrete numbers, not vague
Flag lifecycle
Every flag passes through 6 phases. Skipping any phase creates debt.
request → design → ship → ramp → cleanup → archivePhase 1: Request
Triggered by an engineer or PM identifying a need.
Required:
- Flag name (kebab-case, descriptive:
new-checkout-flownotflag1) - Owner (named individual; not a team)
- Type (Release / Experiment / Operational / Permission)
- Justification (why a flag, not direct deploy?)
- Expected lifespan (days for Release, weeks for Experiment)
Tool: assets/flag_request_template.md
Reject the request if:
- It's a cosmetic change with no risk → ship via deploy
- It has no clear cleanup criteria → not a flag, refactor instead
- It duplicates an existing flag → reuse
Phase 2: Design
Before writing code. Document decisions.
Required artifacts:
- Entry in
docs/feature-flags.md(or your flag registry) with: name, owner, type, kill switch, dashboard URL - Rollout plan generated by
rollout_planner.py - Kill-switch trigger and runbook
- Abort criteria with concrete thresholds
Code location:
- Single point of decision (not 5
if (flag)scattered) - Use a strategy/feature-toggle pattern at module boundary
# Good: one decision at module entry
if flags.is_enabled("new-checkout"):
return new_checkout(request)
return legacy_checkout(request)
# Bad: flag check scattered through the function
def checkout(request):
if flags.is_enabled("new-checkout"):
validate_v2(request)
else:
validate_v1(request)
if flags.is_enabled("new-checkout"):
format_v2(request)
else:
format_v1(request)
# ... many morePhase 3: Ship
Deploy with flag at 0% in production, 100% in dev/staging.
Verification before merge:
- [ ]
kill_switch_audit.pypasses - [ ] Both branches (on/off) covered by tests
- [ ] Provider dashboard shows the flag at 0%
- [ ] Kill switch tested in staging (flip to ON, observe; flip to OFF, observe)
- [ ] Monitoring dashboard linked from flag-doc entry
Common shipping mistakes:
- Default-to-true in production (skip the safety wheels)
- Test only the new path; assume the old path still works
- Forget to update the flag-doc
Phase 4: Ramp
Execute the rollout plan from rollout_planner.py. Hold each phase per rollout_strategies.md.
Decision points:
- After each phase: check abort criteria → hold | rollback | advance
- Communicate progress in team channel
- Update flag-doc with current percent and any abort events
Phase 5: Cleanup
Once at 100% (or experiment concluded with a winner picked), remove the flag.
Cleanup checklist:
- [ ] Flag at 100% for ≥7 days (Release flags) OR test concluded (Experiment)
- [ ] Owner confirms no rollback risk
- [ ] Code change: delete the conditional, keep the new branch, delete the old branch
- [ ] Delete the flag in the provider dashboard
- [ ] Mark the flag-doc entry as ARCHIVED with date and PR link
- [ ] Add to CHANGELOG: "Removed feature flag: <name>"
Common cleanup mistakes:
- Removing the flag from code but forgetting the provider config (orphaned)
- Removing both branches (keep the new one)
- Not updating flag-doc (audit trail lost)
- Not running tests after removal (latent break)
Phase 6: Archive
Move the flag-doc entry to an archive section. Keep the audit trail.
## Archived
### new-checkout-flow [removed 2026-04-12, PR #1234]
- Owner: jane@team
- Type: Release
- Lifespan: 38 days from request to removal
- Outcome: Shipped at 100%; no incidentsLifecycle automation
| Phase | Tool / process |
|---|---|
| Request | flag_request_template.md filled in PR description |
| Design | rollout_planner.py output committed to PR |
| Ship | kill_switch_audit.py as pre-merge CI gate |
| Ramp | Provider dashboard execution; abort wired to alerts |
| Cleanup | Quarterly run of flag_debt_scanner.py |
| Archive | Manual (engineer cleanup PR) |
SLAs by phase
| Phase | Max duration | Trigger if exceeded |
|---|---|---|
| Request → Design | 7 days | Owner ping |
| Design → Ship | 30 days | Owner ping; close request if stale |
| Ship → Ramp start | 7 days | Owner ping |
| Ramp → 100% (Release) | 30 days | Pause, review |
| 100% → Cleanup | 30 days | flag_debt_scanner.py flags it |
| Cleanup → Archive | 7 days | PR review reminder |
Worked example
Day 0: Engineer files request: new-search-relevance Release flag, owner @bob, expected 21-day rollout.
Day 2: Design done. flag-doc entry created. rollout_planner.py output: ring strategy, 5 rings over 14 days. Kill-switch: any drop in CTR > 5%, set flag to 0% via provider API.
Day 4: Code shipped, flag at 0%. kill_switch_audit.py green. Smoke test passes.
Day 5: Ring 1 — 1% rollout. CTR within bounds. Hold 48h.
Day 7: Ring 2 — 5%. p99 latency +5% (within bounds). Hold 48h.
Day 9: Ring 3 — 25%. CTR +2% — winning. Hold 48h.
Day 11: Ring 4 — 50%. CTR +2.5%. Hold 48h.
Day 13: Ring 5 — 100%. Hold 7 days for stability.
Day 20: Cleanup PR opens — remove conditional, delete old branch.
Day 21: PR merged. Flag deleted in provider. flag-doc entry archived.
Total elapsed: 21 days. This is the target.
When the lifecycle breaks
| Symptom | Diagnosis | Fix |
|---|---|---|
| Flag at 100% in code 6+ months | Cleanup phase skipped | Run flag_debt_scanner.py quarterly |
| Flag has no owner | Owner left; not reassigned | Assign to team's tech-debt owner; cleanup or transfer in 30 days |
| Two flags doing the same thing | Request phase missed dedup check | Consolidate; archive duplicate |
| Flag-doc entry missing | Design phase skipped | kill_switch_audit.py must be a CI gate |
| Flag flipped without rollout plan | Ramp phase skipped | Treat as incident; review cause |
Flag taxonomy — the 4 types
Misclassifying a flag is the root cause of flag debt. Pick one type at the moment you create the flag.
Decision tree
Is the flag intended to be permanent (entitlement, plan tier, role-based access)?
├── YES → Permission flag
└── NO → Will it eventually be removed?
├── Will it be removed when feature is fully shipped?
│ └── Yes → Release flag
├── Will it be removed when an A/B test concludes?
│ └── Yes → Experiment flag
└── Will it remain as a circuit breaker / safety toggle?
└── Yes → Operational flag1. Release flag
Purpose: Hide an unfinished or risky feature in production while it's being built or rolled out.
| Property | Value |
|---|---|
| Lifespan | Days to weeks (≤90 days target) |
| Default | OFF in prod, ON in dev/staging |
| Owner | Engineer who created it |
| Cleanup trigger | Reached 100% rollout AND stable for 7+ days |
| Debt risk | High — easy to forget |
| Storage | Provider (LD/GrowthBook) or config file |
Examples:
new-checkout-flow— gating a UI rewritepayment-v2-engine— gating backend rewrite during cutoverenable-search-relevance-v3— A/B test of new ranking
Anti-pattern: Release flag still at 100% in code 6+ months later. The branch the flag protects is dead code; remove it.
2. Experiment flag
Purpose: Run an A/B test or multivariate experiment.
| Property | Value |
|---|---|
| Lifespan | 2-8 weeks (until significance) |
| Default | OFF; control group |
| Owner | Product or Marketing |
| Cleanup trigger | Test concluded; winner shipped |
| Debt risk | Medium |
| Storage | Provider with experimentation features |
Examples:
homepage-headline-v2— testing new copypricing-page-monthly-vs-annual-default— testing default toggleonboarding-checklist-vs-tour— testing onboarding pattern
Anti-pattern: Experiment running for 6 months because no one decided to call it. Either declare a winner or kill the test.
3. Operational flag
Purpose: Circuit breakers, kill switches, performance toggles. Designed to be flipped during incidents.
| Property | Value |
|---|---|
| Lifespan | Months to years (long-lived by design) |
| Default | ON (active path) |
| Owner | SRE / on-call team |
| Cleanup trigger | Replaced by autoscaling, retired feature |
| Debt risk | Low — they're meant to persist |
| Storage | Provider with low-latency global edge |
Examples:
enable-rate-limit-v2— kill switch if v2 misbehavesdisable-recommendations-engine— emergency cutoffuse-fallback-search— degraded mode toggle
Anti-pattern: Operational flag that no one knows how to use during an incident. Document the trigger and runbook.
4. Permission flag
Purpose: Entitlements per user/account/plan/role. Permanent by design.
| Property | Value |
|---|---|
| Lifespan | Indefinite (plan/role lifetime) |
| Default | OFF; granted by entitlement system |
| Owner | Product (plan/role definitions) |
| Cleanup trigger | Plan or role retired |
| Debt risk | Very low |
| Storage | User/account database, NOT a flag provider |
Examples:
feature.advanced-analytics— enterprise-onlyfeature.export-csv— paid plans onlyrole.admin-dashboard— admin-only UI
Anti-pattern: Permission flags stored in a flag provider with per-user targeting rules. Move them to your entitlements system; they're not feature flags.
Classification matrix
When you can't decide, ask:
| Question | If YES | If NO |
|---|---|---|
| Will this be at 100% in <90 days? | Release | next ↓ |
| Will this run an A/B test? | Experiment | next ↓ |
| Is this a kill switch / safety toggle? | Operational | next ↓ |
| Is this a plan/role entitlement? | Permission | reconsider |
If none fit: you don't need a flag. Either ship the feature directly via deploy, or use a different mechanism (config, env var, role).
Ownership rules
- Every flag must have a named owner at creation
- When the owner leaves, the flag is reassigned within 30 days or removed
- Release flags lapse to the team's tech-debt owner if not reassigned
Lifespan SLAs
| Type | Max acceptable lifespan | Cleanup automation |
|---|---|---|
| Release | 90 days | flag_debt_scanner.py |
| Experiment | 60 days | Provider auto-stop on significance |
| Operational | none | Annual review |
| Permission | none | Tied to plan/role retirement |
Provider comparison
Five mainstream providers + DIY. Pick based on flag count, targeting needs, compliance, and self-hosting requirements.
At-a-glance matrix
| Provider | Flag count sweet spot | Targeting | A/B testing | Audit log | Self-host | OSS | Pricing model |
|---|---|---|---|---|---|---|---|
| LaunchDarkly | 100+ | Best-in-class | Yes (Galaxy) | Full SOC2 audit trail | Edge SDK only | No | Per-MAU, expensive |
| GrowthBook | 20-500 | Good | Yes (built-in) | Yes | Yes (Docker/k8s) | Yes (MIT) | Free OSS + Cloud per-MAU |
| Statsig | 50-500 | Good | Best-in-class | Yes (paid) | No | No | Free tier (1M events), then per-MAU |
| Unleash | 10-200 | Good | Limited | Yes (Enterprise) | Yes (Docker/k8s) | Yes (Apache 2) | Free OSS + Hosted/Enterprise |
| Flipt | 5-100 | Basic | No | Limited | Yes (Docker/k8s) | Yes (MIT) | OSS only |
| DIY | <50 | None to basic | None | Whatever you build | Always | N/A | None |
When to choose each
LaunchDarkly
Choose if:
- Enterprise team with 100+ flags across many services
- Compliance requires SOC2 / ISO 27001 / FedRAMP audit logs
- Need fine-grained targeting (cohorts, custom attributes, percentages by attribute)
- Need experimentation + targeting + audit in one platform
- Budget for enterprise tooling ($20-100k/year typical)
Avoid if:
- Small team / <50 flags (overkill)
- Strict data residency (no on-prem; relays only)
- Low budget
GrowthBook
Choose if:
- Mid-market team that wants OSS option for self-hosting
- Need built-in A/B testing with proper stats (frequentist + Bayesian)
- Want SQL-based experimentation (define metrics from your warehouse)
- Self-host on k8s or run their hosted Cloud
Avoid if:
- Need real-time targeting at edge (use LD or Statsig)
- Need enterprise audit features (Cloud only)
Statsig
Choose if:
- Growth/product team for whom experimentation is the core use
- Need advanced stats (CUPED, sequential testing)
- Want generous free tier (good for early-stage)
- Want best-in-class metric library and platform-side experimentation logic
Avoid if:
- Strict data residency / self-host requirement (no on-prem option)
- Don't need experimentation, just toggles (overkill)
Unleash
Choose if:
- OSS-first culture; want to self-host
- Dev-friendly with good SDKs and a clean API
- Don't need full A/B testing platform
- Need Open Source license for compliance (Apache 2)
Avoid if:
- Need experimentation + stats out of the box
- Need enterprise-grade audit (Enterprise tier only)
Flipt
Choose if:
- Lightweight needs, <100 flags
- k8s-native (Flipt is operator-friendly)
- Want pure OSS, no commercial component
- Don't need A/B testing
Avoid if:
- Need targeting beyond simple boolean rules
- Need experimentation
- Need analytics or audit features
DIY (env vars / config file)
Choose if:
- <50 flags total
- No targeting beyond
enabled: true/false - No A/B testing needs
- Want zero external dependencies
- Strict cost control
Implementation:
# config/flags.yaml
flags:
new-checkout: { enabled: true, owner: jane@team }
payment-v2: { enabled: false, owner: bob@team, kill_switch: PagerDuty alert "payment-v2 SEV1" }Or env-var based:
FLAG_NEW_CHECKOUT=true
FLAG_PAYMENT_V2=falseAvoid if:
- Flag count growing past 50
- Need percentage rollouts (you'll re-implement provider logic poorly)
- Need audit log (compliance)
- Multiple teams / multiple deploy cadences
Cost rule of thumb
| Team stage | Typical monthly cost |
|---|---|
| Pre-seed / solo | $0 (DIY or OSS) |
| Seed (Series A) | $0-200 (Statsig free tier, Unleash OSS) |
| Series B-C | $500-3,000 (GrowthBook Cloud, Unleash Pro) |
| Series D+ / Enterprise | $5,000-20,000+ (LaunchDarkly, Statsig Pro, Unleash Enterprise) |
Migration paths
Easy migrations:
- DIY → Unleash / Flipt (similar simple model)
- Unleash ↔ GrowthBook (similar feature surface)
Hard migrations:
- LaunchDarkly → anywhere (proprietary targeting language)
- Statsig → anywhere (proprietary experimentation logic)
Lock-in mitigation: Wrap your provider behind an interface in code:
interface FlagProvider {
isEnabled(name: string, context?: UserContext): boolean;
getValue<T>(name: string, defaultValue: T, context?: UserContext): T;
}Swap providers by writing a new adapter, not by rewriting every call site.
Build-vs-buy threshold
Buy a provider when:
- Flag count > 50
- Multiple teams need to manage flags independently
- Targeting needs include percentages, cohorts, or custom attributes
- Compliance requires audit log
- Need real-time updates without redeploy
Build (DIY) when:
- All of the above are NO
Selection checklist
Before signing a contract:
- [ ] Estimate flag count over 12 months
- [ ] List required targeting dimensions (user/account/geo/%/custom)
- [ ] Confirm SDK availability for every language in your stack
- [ ] Check edge latency (p99 < 50ms for prod)
- [ ] Verify failure mode if provider is unreachable (default-to-safe)
- [ ] Confirm SOC2 / data residency if needed
- [ ] Run a 30-day proof-of-concept; measure actual cost at projected MAU
Rollout strategies
Pick a strategy by risk, not by preference. Higher-risk launches get slower, more granular ramps.
The 4 strategies
1. Ring (canary) — risky launches
1% → 5% → 25% → 50% → 100%
| Property | Value |
|---|---|
| Use when | Touches payments, auth, data integrity, performance-sensitive paths |
| Duration | 14-30 days typical |
| Hold time per ring | 24-72 hours minimum (long enough to detect anomalies) |
| Abort cost | Low (only 1-25% affected) |
| Verification | Full metrics suite at each ring |
Phases: 1. 0% (deploy) — code ships dark; verify it deploys without flag turned on 2. 1% — internal users + low-traffic cohort; full metric verification 3. 5% — broader smoke test; watch for tail-of-distribution issues 4. 25% — significant load; performance and infra checks 5. 50% — half-and-half; perfect for A/B comparison 6. 100% — fully on; hold 7 days before removing flag
Abort triggers per ring:
- Error rate > baseline + 1pp
- p99 latency > baseline × 1.2
- Business metric regression (conversion, retention) > baseline × 0.95
2. Linear — medium risk
Constant percent-per-day until target.
| Property | Value |
|---|---|
| Use when | Standard feature launches without high-risk paths |
| Duration | 7-14 days |
| Step size | (target / duration_days) per day |
| Abort cost | Medium |
| Verification | Daily metric check |
Example: 100% over 10 days = 10% per day.
3. Log (front-loaded) — low risk
Fast early ramp, slow tail. Reaches majority of population in first 1/3 of duration.
| Property | Value |
|---|---|
| Use when | Low-risk launch with high confidence; UI tweaks; copy changes |
| Duration | 3-7 days |
| Curve | pct(t) = target × log(1+t) / log(1+T) |
| Abort cost | Higher (most users on early) |
| Verification | Light — metric check at start and end |
4. Cohort — entitlement-aware
Named segments rolled in order: internal → beta → free → paid → all
| Property | Value |
|---|---|
| Use when | Feature has different value/risk per cohort; beta access; paying-tier first |
| Duration | Variable (gate by cohort size, not days) |
| Step size | Whole cohort at a time |
| Abort cost | Cohort-bounded |
| Verification | Per-cohort metrics |
Order rules: 1. Internal first — your own team finds bugs cheaply 2. Beta opt-in users — they expect rough edges 3. Free tier — broader signal at lower commercial risk 4. Paid plans — most valuable users last (or first for premium features) 5. All — flag fully on; remove flag
Geo-staged variant
For internationally-distributed products, layer geo on top of any strategy:
Phase A: 100% in NZ/AU (low-traffic, English, off-business-hours US)
Phase B: 100% in EU (test data residency / GDPR paths)
Phase C: 100% in US (high traffic; full validation)Useful for catching i18n, timezone, and regional infrastructure issues before peak load.
Abort criteria
Hard-coded thresholds that auto-flip the flag back to 0% (or trigger paging):
| Signal | Threshold | Severity |
|---|---|---|
| Error rate (5xx) | > baseline + 1 percentage point | SEV1 |
| Error rate (4xx) | > baseline + 5 percentage points | SEV2 |
| p99 latency | > baseline × 1.2 | SEV2 |
| p999 latency | > baseline × 1.5 | SEV1 |
| Conversion rate | < baseline × 0.95 | SEV2 |
| Retention (D1/D7/D30) | < baseline × 0.95 | SEV2 |
| Database CPU | > 80% | SEV1 |
| Saturation alarm | any | SEV1 |
Automate: wire each threshold to a webhook that sets the flag to 0% via provider API.
Verification per phase
At each phase, confirm:
1. Health metrics are within abort thresholds 2. Business metrics match or exceed control 3. Logs show no new error patterns 4. User reports (support tickets) show no spike for the affected feature 5. Ops on-call acknowledges no anomalies
If any signal is off, hold the phase. Don't advance on schedule alone.
Hold-time rules
- Off-hours hold time doesn't count toward bake-in (e.g., a phase started Friday 6pm in PST is held until Monday 9am)
- Weekend rollouts require explicit owner approval and on-call coverage
- Holiday rollouts require VP-level approval
Common mistakes
| Mistake | Fix |
|---|---|
| Skipping rings to "just get it done" | Don't. Aborts cost less than incidents. |
| 100% on Friday afternoon | Wait until Monday morning. |
| Rolling forward when metrics regress slightly | Stop. Investigate. The next ring exposes 5× more users. |
| No verification step defined per ring | Define it before starting. |
| Manual abort only (no automated kill switch) | Wire a threshold-based auto-abort. |
| Holding "for a few hours" then forgetting | Set a calendar event with the next phase + abort criteria. |
Tools
scripts/rollout_planner.py— generates a markdown plan- Provider dashboards — for execution and real-time abort
- Metrics dashboard linked from
flag-docentry - On-call runbook with kill-switch trigger words
#!/usr/bin/env python3
"""Scan a repo for stale feature flags (Karpathy goal-driven cleanup).
Detects flag identifiers from common code patterns, dates each one by its
introducing commit, and flags items older than --max-age-days that appear in
fewer than --min-uses places as cleanup candidates.
"""
import argparse
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
from datetime import datetime, timezone
FLAG_PATTERNS = [
re.compile(r'\b(?:isFlagEnabled|isEnabled|featureFlag|getFlag|flag|useFlag|useExperiment)\(\s*["\']([\w.\-:]+)["\']'),
re.compile(r'\b(?:client|ld|unleash|growthbook|statsig)\.(?:variation|isEnabled|feature|getValue|getExperiment)\(\s*["\']([\w.\-:]+)["\']'),
]
CODE_EXTS = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rb", ".java", ".kt", ".cs", ".rs", ".php"}
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "dist", "build", "__pycache__", ".next"}
def _walk_code_files(repo):
for root, dirs, files in os.walk(repo):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in files:
if os.path.splitext(f)[1] in CODE_EXTS:
yield os.path.join(root, f)
def _scan_file(path):
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError:
return []
found = set()
for pat in FLAG_PATTERNS:
for m in pat.finditer(text):
found.add(m.group(1))
return list(found)
def _first_commit_date(repo, flag_name):
try:
out = subprocess.run(
["git", "-C", repo, "log", "--diff-filter=A", "--format=%cI", "-S", flag_name],
capture_output=True, text=True, timeout=10, check=False,
)
except (subprocess.SubprocessError, OSError):
return None
lines = [ln for ln in out.stdout.strip().split("\n") if ln]
if not lines:
return None
try:
return datetime.fromisoformat(lines[-1])
except ValueError:
return None
def _age_days(when):
if when is None:
return None
now = datetime.now(timezone.utc)
return (now - when).days
def collect_flags(repo):
flags_to_paths = defaultdict(list)
for path in _walk_code_files(repo):
for name in _scan_file(path):
flags_to_paths[name].append(os.path.relpath(path, repo))
return flags_to_paths
def assess(repo, flags_to_paths, max_age_days, min_uses):
rows = []
for name in sorted(flags_to_paths.keys()):
paths = flags_to_paths[name]
when = _first_commit_date(repo, name)
age = _age_days(when)
is_debt = (
age is not None
and age > max_age_days
and len(paths) <= min_uses
)
rows.append({
"flag": name,
"uses": len(paths),
"age_days": age,
"first_seen": when.date().isoformat() if when else None,
"files": paths[:5],
"is_debt": is_debt,
})
return rows
def render_text(rows, max_age_days):
debt = [r for r in rows if r["is_debt"]]
print(f"Flag Debt Scanner — {len(rows)} flags found, {len(debt)} stale (>{max_age_days}d, ≤2 uses)")
print("")
if not debt:
print("No debt detected. Nice.")
return
print(f"{'flag':40} {'age':>6} {'uses':>4} files")
print("-" * 80)
for r in debt:
files = ", ".join(r["files"][:2]) + ("…" if len(r["files"]) > 2 else "")
age = f"{r['age_days']}d" if r["age_days"] is not None else "?"
print(f"{r['flag']:40} {age:>6} {r['uses']:>4} {files}")
print("")
print("Suggested action: confirm reached 100% (or killed); delete dead branch; remove flag.")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--repo", default=".", help="Path to repo root (default: .)")
ap.add_argument("--max-age-days", type=int, default=90, help="Flags older than this are debt candidates (default: 90)")
ap.add_argument("--min-uses", type=int, default=2, help="Flags with ≤ this many uses are debt candidates (default: 2)")
ap.add_argument("--format", choices=["text", "json"], default="text")
args = ap.parse_args()
repo = os.path.abspath(args.repo)
if not os.path.isdir(os.path.join(repo, ".git")):
print(f"WARN: {repo} is not a git repo; age detection disabled", file=sys.stderr)
flags = collect_flags(repo)
rows = assess(repo, flags, args.max_age_days, args.min_uses)
if args.format == "json":
print(json.dumps(rows, indent=2, default=str))
else:
render_text(rows, args.max_age_days)
return 1 if any(r["is_debt"] for r in rows) else 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""Verify every feature flag in code has a documented kill switch.
Cross-references flag identifiers found in source code against a markdown
flag registry. Each documented flag must declare: owner, type, kill switch,
dashboard. Reports undocumented flags (FAIL) and incompletely-documented
flags (WARN). Use as a pre-merge gate.
"""
import argparse
import json
import os
import re
import sys
FLAG_PATTERNS = [
re.compile(r'\b(?:isFlagEnabled|isEnabled|featureFlag|getFlag|flag|useFlag|useExperiment)\(\s*["\']([\w.\-:]+)["\']'),
re.compile(r'\b(?:client|ld|unleash|growthbook|statsig)\.(?:variation|isEnabled|feature|getValue|getExperiment)\(\s*["\']([\w.\-:]+)["\']'),
]
REQUIRED_FIELDS = ("owner", "type", "kill switch", "dashboard")
CODE_EXTS = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rb", ".java", ".kt", ".cs", ".rs", ".php"}
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "dist", "build", "__pycache__", ".next"}
def _walk_code_files(repo):
for root, dirs, files in os.walk(repo):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in files:
if os.path.splitext(f)[1] in CODE_EXTS:
yield os.path.join(root, f)
def discover_code_flags(repo):
found = set()
for path in _walk_code_files(repo):
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError:
continue
for pat in FLAG_PATTERNS:
for m in pat.finditer(text):
found.add(m.group(1))
return found
def _split_sections(text):
"""Split flag-doc into per-flag sections by H2 (## flag-name) or H3."""
sections = {}
current = None
buf = []
for line in text.splitlines():
m = re.match(r"^#{2,3}\s+([\w.\-:]+)\s*$", line)
if m:
if current is not None:
sections[current] = "\n".join(buf)
current = m.group(1)
buf = []
else:
buf.append(line)
if current is not None:
sections[current] = "\n".join(buf)
return sections
def _missing_fields(section_text):
lower = section_text.lower()
return [f for f in REQUIRED_FIELDS if f not in lower]
def audit(repo, flag_doc_path):
if not os.path.isfile(flag_doc_path):
return {"error": f"flag-doc not found: {flag_doc_path}"}
with open(flag_doc_path, "r", encoding="utf-8") as f:
doc_text = f.read()
sections = _split_sections(doc_text)
documented = set(sections.keys())
code_flags = discover_code_flags(repo)
undocumented = sorted(code_flags - documented)
orphaned_docs = sorted(documented - code_flags)
incomplete = []
for name in sorted(code_flags & documented):
missing = _missing_fields(sections[name])
if missing:
incomplete.append({"flag": name, "missing": missing})
return {
"code_flags": sorted(code_flags),
"documented_flags": sorted(documented),
"undocumented": undocumented,
"incomplete": incomplete,
"orphaned_in_doc": orphaned_docs,
}
def render_text(result):
if "error" in result:
print(f"ERROR: {result['error']}")
return
code, doc = result["code_flags"], result["documented_flags"]
print(f"Kill Switch Audit — {len(code)} flags in code, {len(doc)} documented")
print("")
if result["undocumented"]:
print(f"FAIL: {len(result['undocumented'])} undocumented flag(s):")
for f in result["undocumented"]:
print(f" - {f}")
print("")
if result["incomplete"]:
print(f"WARN: {len(result['incomplete'])} flag(s) with incomplete documentation:")
for item in result["incomplete"]:
print(f" - {item['flag']}: missing {', '.join(item['missing'])}")
print("")
if result["orphaned_in_doc"]:
print(f"INFO: {len(result['orphaned_in_doc'])} doc entry(s) for flags not in code:")
for f in result["orphaned_in_doc"]:
print(f" - {f}")
print("")
if not (result["undocumented"] or result["incomplete"]):
print("PASS: every code flag is fully documented.")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--repo", default=".", help="Path to repo root (default: .)")
ap.add_argument("--flag-doc", required=True, help="Path to markdown flag registry (e.g., docs/feature-flags.md)")
ap.add_argument("--format", choices=["text", "json"], default="text")
args = ap.parse_args()
result = audit(os.path.abspath(args.repo), args.flag_doc)
if args.format == "json":
print(json.dumps(result, indent=2))
else:
render_text(result)
if "error" in result:
return 2
if result["undocumented"] or result["incomplete"]:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""Generate a phased rollout schedule for a feature flag.
Strategies:
ring 1% → 5% → 25% → 50% → 100% — risky launches
linear constant percent-per-day — medium risk
log fast early, slow tail — low risk
cohort named cohorts (internal → beta → free → paid → all) — entitlement-aware
"""
import argparse
import json
import math
import sys
from datetime import datetime, timedelta
DEFAULT_RING_STOPS = [1, 5, 25, 50, 100]
DEFAULT_COHORTS = ["internal", "beta", "free", "paid", "all"]
def _ring(target):
return [s for s in DEFAULT_RING_STOPS if s <= target] + ([target] if target not in DEFAULT_RING_STOPS else [])
def _linear(target, days):
if days < 1:
return [target]
step = target / days
return [round((i + 1) * step, 2) for i in range(days)]
def _log_curve(target, days):
if days < 1:
return [target]
out = []
for i in range(days):
frac = math.log1p(i + 1) / math.log1p(days)
out.append(round(target * frac, 2))
return out
def _dedupe_sorted(values):
seen = set()
out = []
for v in values:
if v not in seen:
seen.add(v)
out.append(v)
return out
def build_schedule(strategy, target, duration_days, population, start_date):
if strategy == "ring":
percents = _ring(target)
elif strategy == "linear":
percents = _linear(target, duration_days)
elif strategy == "log":
percents = _log_curve(target, duration_days)
elif strategy == "cohort":
per_step = target / len(DEFAULT_COHORTS)
percents = [round(per_step * (i + 1), 2) for i in range(len(DEFAULT_COHORTS))]
else:
raise ValueError(f"unknown strategy: {strategy}")
percents = _dedupe_sorted(percents)
n = len(percents)
interval = max(1, duration_days // max(n - 1, 1))
rows = []
for i, pct in enumerate(percents):
date = start_date + timedelta(days=i * interval)
users = int(population * pct / 100)
cohort = DEFAULT_COHORTS[min(i, len(DEFAULT_COHORTS) - 1)] if strategy == "cohort" else None
rows.append({
"phase": i + 1,
"date": date.date().isoformat(),
"percent": pct,
"users": users,
"cohort": cohort,
"abort_if": "error_rate > baseline + 1pp OR p99_latency > baseline * 1.2",
"verify": "compare metrics dashboard against control",
})
return rows
def render_markdown(rows, strategy, target, duration_days, population):
print(f"# Rollout plan — strategy={strategy}, target={target}%, duration={duration_days}d, population={population:,}")
print("")
headers = ["Phase", "Date", "Percent", "Users", "Cohort", "Abort criteria", "Verify"]
print("| " + " | ".join(headers) + " |")
print("|" + "|".join(["---"] * len(headers)) + "|")
for r in rows:
cohort = r["cohort"] or "—"
print(f"| {r['phase']} | {r['date']} | {r['percent']}% | {r['users']:,} | {cohort} | {r['abort_if']} | {r['verify']} |")
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--population", type=int, required=True, help="Total user population")
ap.add_argument("--target-percent", type=float, default=100, help="Final rollout percent (default: 100)")
ap.add_argument("--duration-days", type=int, default=14, help="Total rollout duration (default: 14)")
ap.add_argument("--strategy", choices=["ring", "linear", "log", "cohort"], default="ring")
ap.add_argument("--start-date", default=None, help="ISO date YYYY-MM-DD (default: today)")
ap.add_argument("--format", choices=["markdown", "json"], default="markdown")
args = ap.parse_args()
if not 0 < args.target_percent <= 100:
print("ERROR: --target-percent must be in (0, 100]", file=sys.stderr)
return 2
if args.population < 1:
print("ERROR: --population must be >= 1", file=sys.stderr)
return 2
start = datetime.fromisoformat(args.start_date) if args.start_date else datetime.utcnow()
rows = build_schedule(args.strategy, args.target_percent, args.duration_days, args.population, start)
if args.format == "json":
print(json.dumps(rows, indent=2, default=str))
else:
render_markdown(rows, args.strategy, args.target_percent, args.duration_days, args.population)
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Use feature-flags-architect for lifecycle, rollout planning, and debt audits; pick provider docs when you only need SDK wiring without cleanup discipline.
FAQ
What tools ship with feature-flags-architect?
feature-flags-architect bundles three stdlib Python scripts: flag_debt_scanner.py for stale flags, rollout_planner.py for ring/linear/log/cohort schedules, and kill_switch_audit.py to verify documented kill paths against repo references.
Which flag types does feature-flags-architect define?
feature-flags-architect defines Release, Experiment, Operational, and Permission flags with distinct lifespans and owners. Only Release and Experiment flags belong on debt-scanner watchlists; Operational and Permission flags are intentionally long-lived.
Does feature-flags-architect require pip packages?
feature-flags-architect version 2.9.0 uses stdlib-only Python tools with no pip installs. Run scripts with --help, point kill_switch_audit.py at docs/feature-flags.md, and invoke the /flag-cleanup slash command from a git repository.