Featurevisor
Open source · Git based · AI agent ready

Feature flags & configsas code

Flags, experiments and remote configuration, declared as YAML in your Git repository.

my-featurevisor-project
features/checkout.yml
variables/supportEmail.yml
description: Checkout redesign
bucketBy: userId
rules:
- segments: netherlands # Dutch visitors
percentage: 100 # enabled for all
- segments: "*" # everyone else
percentage: 25 # gradual rollout
two files, four visitorsin memory
country: nluserId: 8f2a
country: nluserId: c4e1
country: deuserId: 41c7
country: deuserId: 9b30
f.isEnabled('checkout', context)
f.getVariable('supportEmail', context)
// → both from memory, no network call

Manage with Git

Three steps, all of them yours

A pull request to your repository, a build in your CI, a file on your CDN. There is no vendor in the middle.

A team sending a pull request to a Git repository

Send pull requests

Attributes, segments, features and variables are files in one repository. Every change arrives as a pull request and is reviewed like any other.

What lives in the repository
Datafiles built by Featurevisor and uploaded to a CDN

Build and upload to your CDN

Your CI lints and tests on the pull request, then builds the datafiles on merge and uploads them to your CDN as static JSON.

How datafiles are built
Applications reading a datafile through the SDKs

Fetch and evaluate with SDKs

Applications fetch the datafile and evaluate every flag and variable in memory, on the web, on mobile and on the server. No request per lookup.

Pick an SDK

Three types of evaluation

More than just feature flags

One repository, one pipeline, one SDK call shape. What changes is the kind of answer you get back.

Feature flags
Feature
checkout
matched rulenetherlands · 100%
resolves totrue

On or off, by rule

Rules are checked in order and the first match wins. The plain boolean case stays plain.

f.isEnabled('checkout', context)
// → true

Rollout by rule

netherlands
100%
everyone else
25%
A/B experiments
Feature · 2 variations
pricing
controlweight 50
treatmentweight 50

One variation, by weight

Weights divide the audience that already passed the rules, and a user keeps the arm they were given.

f.getVariation('pricing', context)
// → 'treatment'

Traffic split

control · 50treatment · 50
Variables
Global variable · string
supportEmail
overridenetherlands
resolves to'[email protected]'

Typed values, validated

Strings, numbers, objects and JSON, checked against a schema when the datafile is built.

f.getVariable('supportEmail', context)

Supported types

stringintegerdoublebooleanarrayobjectjson

Gradual rollout

Ship it to ten percent, then everyone

The percentage is a slice of the audience your rule already matched. Bucketing is deterministic, so the same users stay in as you ramp, and nobody flickers between experiences.

pick a step, the audience follows
features/checkout.yml
bucketBy: userId
rules:
- key: everyone
segments: '*'
percentage: 10
1 dot = 1% of the audience10 in

userId 41c7 is in at 10%, and at every step after

0100
10%

Targeting

Describe the audience once

Attributes become segments, segments become rules. Every layer is a file you can review, reuse and test on its own.

01Declare an attribute
attributes/country.yml
description: Visitor country
type: string
# your app passes this
# as context at runtime

One typed input, declared once. Anything your app can pass as context: country, plan, app version.

02Group them into a segment
segments/netherlands.yml
description: Dutch visitors
conditions:
- attribute: country
operator: equals
value: nl

Named, reusable conditions. Written once, referenced everywhere.

03Target a rule with it
features/checkout.yml
rules:
- segments: netherlands
percentage: 100
- segments: "*"
percentage: 25

The rule names the segment from step two. Rules are checked in order, and the first match wins.

Segmentnetherlands
  • features/pricing.yml
  • features/liveChat.yml
  • features/checkout.yml

Global variables

Not everything is on or off

Plenty of what you want to change at runtime is a value, not a switch. A global variable is a typed value in a file of its own, with its own lifecycle and its own targeting.

variables/supportEmail.yml
description: Support email shown to users
type: string
defaultValue: [email protected]
Global variable · string
supportEmail
requiresno feature
bucketingnone
resolves to'[email protected]'
One value, one key
f.getVariable('supportEmail')

It is addressed by its own key, so no feature has to be opened first and no bucketing has to be passed. Every type the schema system supports is available, all validated when the datafile is built. That is the whole file. Narrowing it comes next.

Overrides

Values that narrow as they go

One default value is rarely the whole story. An override narrows it for the accounts its segment matches, and can carry children that narrow it again, so each level states only what it changes.

variables/workspaceLimits.yml
type: object
defaultValue:
maxSeats: 5
retentionDays: 30
overrides:
- key: paid
segments: paidAccounts
mutate:
maxSeats: 50
overrides:
- key: enterprise
segments: enterpriseAccounts
mutate:
retentionDays: 365
An enterprise account
defaultValuemaxSeats 5 · retentionDays 30
paidsegments paidAccountsmaxSeats 550
enterprisesegments enterpriseAccountsretentionDays 30365
Resolved value
maxSeats50from paid
retentionDays365from enterprise

A child starts from its parent's resolved value, so nothing is restated. Featurevisor flattens the whole tree at build time and ships complete values, which means SDKs never merge anything at runtime.

Feature variables

Or bound to a variation

The same typed values can live inside a feature instead. There they follow whichever variation the user was bucketed into, so each arm of an experiment carries a complete set of its own.

features/buyButton.yml
variablesSchema:
label:
type: string
defaultValue: Buy
colour:
type: string
defaultValue: "#111827"
variations:
- value: control
weight: 50
- value: treatment
weight: 50
variables:
label: Buy now
colour: "#2563eb"
The resolved column
weights
variationcontroltreatment
label'Buy''Buy now'
colour'#111827''#2563eb'
userId 8f2a
f.getVariation('buyButton', context)
// → 'treatment'
f.getVariable('buyButton', 'label', context)
// → 'Buy now'

The variation is decided first, by weight, and every variable follows it. An arm never mixes values from another, and anything it does not set falls back to the schema default.

So which one do you reach for?

The difference shows up at the call site. Count the keys.

Owned by a feature
Feature
buyButton
variablesSchema
label'Buy now'

resolved from variation treatment

Use it when the value only makes sense for that feature, or has to follow a variation. It lives and dies with its feature.

f.getVariable('buyButton', 'label')
// two keys
Standing on its own
Global variable · string
supportEmail
variables/
supportEmail'support-nl@…'

resolved from override netherlands

Use it when the value has its own lifecycle, or is shared across features and services. It does not need percentage rollout.

f.getVariable('supportEmail')
// one key

Dependencies

Things that wait for other things

A feature can require another feature, or one specific variation of it. So can a standalone value. The requirement is checked first, and the rest is never reached.

my-featurevisor-project
features/liveChat.yml
features/chatFileUpload.yml
description: File upload inside chat
bucketBy: userId
# there is nothing to upload into
# unless live chat is on
requiredFeatures: liveChat
rules:
- key: everyone
segments: '*'
percentage: 100
Checked from the top down
liveChatFeature
requiredFeatures
chatFileUploadFeature
satisfiedits own rules decide
not satisfiedfalse, rules never reached

A standalone value can name the same field and wait on a feature in exactly this way. Featurevisor resolves the whole chain at build time, so a datafile never arrives missing something it depends on.

Environments

The same file, two different answers

Rules live under an environment key, so staging and production travel together in one reviewed change instead of drifting apart in two dashboards.

stagingeveryone · 100%

Wide open, so the team lives on it every day.

productioneveryone · 10%

Deliberate, and ramped on its own schedule.

features/checkout.yml
description: Checkout redesign
bucketBy: userId
rules:
staging:
- key: everyone
segments: '*'
percentage: 100
production:
- key: everyone
segments: '*'
percentage: 10

Sets and promotions

Or keep the lanes fully apart

When environments in one file are not enough, sets split the project into independent trees. The same feature key can behave completely differently in each, and you move work forward one promotion at a time.

devwide open
everyone · 100%
sets/dev/features/checkout.yml
stagingteam only
employees · 100%
sets/staging/features/checkout.yml
productionramped
everyone · 10%
sets/production/features/checkout.yml
One tree per set
files
sets/
dev/
features/
segments/
staging/
production/

Each set owns its own attributes, segments, features, targets and tests, exactly like a regular project.

Promote, then review the diff
terminal
npx featurevisor promote --from=dev --to=staging --apply
features/checkout.yml updated
tests/features/checkout.spec.yml updated
dependencies carried along when a rule needs them
2 definitions written to staging

Without --apply it only previews. Applied definitions are ordinary files, so the change still arrives as a Git diff you review, lint and test before it merges.

The workflow

Nothing takes effect until it ships

There is no dashboard where a stray click changes production. Every change travels the same reviewed path your code does.

edit
change a YAML file
review
open a pull request
merge
approved like code
ci
lint, test, build
deploy
static JSON on a CDN
refresh
apps pick it up
Where a value came from

Your feature management history is your Git history. Every value that ever reached production is attributable to a commit, a reviewer and a build.

Feature · production
checkout
valueeveryone · 25%
commita1b2c3d · ramp checkout to 25%
review#482 · approved by two
build#128 · lint, test, 3 datafiles

Skills for AI agents

Ask in English, review it as code

Featurevisor ships an official skill, so an agent can author definitions the way you would. It writes the same YAML, into the same repository, and it still has to get past the same gate.

01You ask

/featurevisor ramp showWishlist to 25%, but only in the Netherlands

Plan

  • reuse segments/netherlands.yml
  • add a rule to features/showWishlist.yml, ahead of the catch-all
  • extend tests/features/showWishlist.spec.yml

No YAML and no dashboard. Describe the outcome and let the skill supply the vocabulary, whether or not you know the words for it.

02It writes definitions
rules:
- key: netherlands
segments: netherlands
percentage: 25
- key: everyone
segments: '*'
percentage: 0

A targeted rule ahead of the catch-all, in the order that decides the answer. Ordinary files, in your repository, in your format.

03You review
segments/netherlands.ymlreused
features/showWishlist.ymlupdated
tests/features/showWishlist.spec.ymlupdated
featurevisor lintpassed
featurevisor test4 specs

It opens a pull request. An agent cannot reach production any faster than you can, because nothing here bypasses the pipeline.

npx skills add featurevisor/featurevisor
Claude CodeCursorCodexOpenCodeand more
Read the skill docs

Confidence

Assert what a user will get, before they get it

Test specs are YAML next to your definitions. Pin a bucket value and a context, state the expected answer, and let CI hold you to it.

The spec
tests/features/checkout.spec.yml
feature: checkout
assertions:
- description: Dutch users are in
at: 40
context:
country: nl
expectedToBeEnabled: true
- description: Everyone else waits
at: 90
context:
country: de
expectedToBeEnabled: false
The run
terminal
npx featurevisor test
tests/features/checkout.spec.yml
Dutch users are in
Everyone else waits
tests/variables/supportEmail.spec.yml
Uses the Netherlands override
All 3 assertions passed

The same command runs locally and in CI. A failing assertion exits non zero, so a rollout that would have surprised someone never reaches the datafile.

Zero latency

Evaluated inside your app, not on our servers

Your project compiles to static JSON. Applications fetch it once, hold it in memory, and evaluate locally. There is no request to make and nothing to be down.

One project
features/ · segments/ · variables/
  • targets/web.ymlfeaturevisor-web.json
  • targets/ios.ymlfeaturevisor-ios.json
  • targets/checkout.ymlfeaturevisor-checkout.json

A target is any runtime that should receive its own datafile. It can take everything, or narrow down by tags and key patterns when an app only needs part of the project.

The artifact
featurevisor-web.jsonrev 128

One file, served from your own CDN. No vendor in the request path, no per seat pricing, and no evaluation service to keep alive.

Same user, every surface
f.getVariation('pricing', context)
webuserId: '8f2a''treatment'
backenduserId: '8f2a''treatment'
mobileuserId: '8f2a''treatment'
The runtimes

The same deterministic bucketing in every SDK, so a user bucketed into an experiment on the web is in the same arm on your backend and on their phone, and the same test specs can be run through the SDKs themselves.

Browse the SDKs

Use cases

What teams use it for

The same definitions, targeting and pipeline cover all of them. Nothing here needs a different product.

Progressive delivery

Ramp a change from one percent to everyone, on your own schedule, with the same users carried forward at every step.

Read the guide

A/B and multivariate tests

Weighted variations with their own variable sets, connected to whichever analytics tool you already run.

Read the guide

Remote configuration

Typed values that change what the app renders without a release, validated against schemas at build time.

Read the guide

Trunk-based development

Merge to one branch several times a day and let unfinished work sit behind a flag rather than behind a long-lived branch.

Read the guide

Testing in production

Expose work in progress to your own team first, then widen, without maintaining a separate build.

Read the guide

Microfrontends

Independent teams shipping on independent cadences, from one repository and one set of datafiles.

Read the guide

Ready to ship with confidence?

Self hosted, free, and yours to run. Start a project in one command.