Getting started
How Featurevisor works
Featurevisor has three parts: a repository of definitions, a pipeline that turns them into datafiles, and SDKs that evaluate those datafiles in your application. This page follows one feature, checkout, through all three.
The repository#
Definitions are YAML files in a Git repository, kept separate from your applications. Each kind has its own directory, and files refer to each other by key.
01Attribute
A value your application passes in.
description: Countrytype: string02Segment
A named audience, built on the attribute.
uses country
description: The Netherlandsconditions: - attribute: country operator: equals value: nl03Feature
Rules that use the segment, checked in order.
uses netherlands
description: Checkout redesignbucketBy: userIdrules: - key: netherlands conditions: netherlands percentage: 100 - key: everyone conditions: '*' percentage: 25Your application supplies country when it asks for a value. The netherlands segment names everyone whose country is nl. The checkout feature is enabled for that segment, then for 25% of everyone else. bucketBy: userId keeps each user's answer stable.
Building blocks#
A project uses these directories by default. Change them in configuration.
| Building block | Directory | Holds |
|---|---|---|
| Attributes | attributes/ | Context values that conditions read |
| Segments | segments/ | Reusable groups of conditions |
| Features | features/ | Flags, variations and feature variables |
| Variables | variables/ | Global variables with overrides |
| Targets | targets/ | What each datafile contains |
| Schemas | schemas/ | Reusable variable types |
| Tests | tests/ | Specs that check results |
Start with only the definitions your application needs. You do not need every building block to use Featurevisor.
The pipeline#
A change to checkout reaches users through the same review and CI process as your code. Featurevisor provides the tools at each stage. Your team owns the process around them.
- Propose
- Featurevisor
- Definitions are plain files with a readable diff.
- You
- Open a pull request and choose who reviews it.
- Check
- Featurevisor
lintvalidates definitions.testruns your specs.- You
- Run both in CI and block merging when they fail.
- Build
- Featurevisor
buildwrites one datafile per target, plus a revision and state files.- You
- Build on merge and commit the state files back.
- Publish
- Featurevisor
- Datafiles are static JSON with no service behind them.
- You
- Upload them to your CDN and decide when apps refresh.
Testing, Building datafiles, State files and Deployment cover each stage in detail.
A release is therefore a change to these files, not to your application. Anyone who can open a pull request can propose one, such as a product manager widening a rollout, and the feature's owners review it like code. Once the new datafile is published, applications that fetch it pick up the change without a new build of their own.
What a datafile contains#
A datafile is the compiled form of your definitions. This is the checkout entry from the one built for the project above:
{ "schemaVersion": "4", "revision": "2", "features": { "checkout": { "bucketBy": "userId", "traffic": [ { "key": "netherlands", "conditions": "netherlands", "percentage": 100000 }, { "key": "everyone", "conditions": "*", "percentage": 25000 } ] } }}The datafile also carries the netherlands segment's conditions, so the SDK can match them without your repository. Percentages are stored with three decimal places, so 25 becomes 25000. Targets decide which features and global variables each datafile contains, so every application loads only what it needs.
Anyone can read a datafile sent to a browser or a phone. Keep secrets out of your definitions, and keep authorization on your server.
The application#
Your application fetches a datafile, gives it to an SDK and asks for values. The SDK evaluates in memory, without a network request for each decision. Fetching and refreshing the datafile are up to your application.
Context#
Context describes who or what is asking: a user, an account, a device or a request. One datafile gives each context its own answer, so you never build a datafile per user.
const context = { userId: 'user-4', country: 'nl',}f.isEnabled('checkout', context) // trueHow a feature decides#
The SDK checks these in order and stops at the first one that decides:
- 01StickyA value pinned in the SDK always wins.
- 02ForceThe first matching force entry decides.
- 03ConditionsAn unmet root gate disables it.
- 04BucketCompute a stable position from the user and feature key.
- 05RulesUse the first matching rule, then check its percentage.
Bucketing turns the bucketBy value and the feature key into a stable position from 0 to 100. A user is in when their position falls inside the matching rule's percentage. Here is checkout for three users:
| Context | First matching rule | Bucket | isEnabled |
|---|---|---|---|
| user-4 · nl | netherlands at 100% | 89.3 | true |
| user-5 · de | everyone at 25% | 15.5 | true |
| user-9 · de | everyone at 25% | 38.5 | false |
user-4 matches netherlands, which is at 100%, so any position is inside. user-5 and user-9 both fall through to everyone, but only user-5 sits inside its 25%. A user keeps the same position for this feature on every device and in every SDK.
Check a context yourself with the CLI. --explain names the rule that matched and the user's bucket:
A feature's variations and variables follow from its flag. The evaluation flow lists every step, and Bucketing and Conditions cover the details.
Variations#
A feature with variations also gives each enabled user one of its values. Weights split the users, and bucketing keeps each user in the same variation:
description: Pricing page experimentbucketBy: userIdvariations: - value: control weight: 50 - value: treatment weight: 50rules: - key: everyone conditions: '*' percentage: 100Your application reads the variation it was given:
const context = { userId: 'user-2' }f.getVariation('pricing', context) // 'treatment'user-2 lands at 70.3 and gets treatment. user-4 lands at 35.7 and gets control, on the other side of the 50/50 split. See Variations.
Global variables#
Global variables decide differently. They have no bucketBy and no percentages. A global variable starts from its default and takes the value of the first matching override:
description: Support email shown in checkouttype: stringoverrides: - key: netherlands conditions: netherlandsYour application reads the value for its context:
const context = { country: 'nl' }f.getVariable('supportEmail', context)// '[email protected]'Every context outside the Netherlands gets the default, [email protected]. An override with continue: true lets later overrides change the value further. See global variables.
As your project grows#
Each of these is optional. A project without environments uses plain rule lists, like the checkout file above.
| When you need | Use |
|---|---|
| Different rules for staging and production | Environments |
| A smaller datafile for each application | Targets and tags |
| Keys grouped by team or product area | Namespaces |
| Separate collections of definitions | Sets |
| Reviewed changes moved between those sets | Promotions |
Build it yourself#
The Quick start creates a project like this one, tests a feature and evaluates it in an application.

