Migration guides
Migrating from v2 to v3
- Migrate in two steps
- Project configuration
- Environments are off by defaultBreaking
- Split environment files removedBreaking
- Scopes replaced by targetsBreaking
- Namespace separatorBreaking
- SetsNew
- PromotionsNew
- Defining features
- Testing features
- CLI usage
- JavaScript SDK usage
- Upgrade to latest SDKNew
- Creating an instanceBreaking
- Instance type renamedBreaking
- Options type renamedBreaking
- setDatafile merges by defaultSoft breaking
- Sticky values belong to instancesBreaking
- Diagnostics instead of loggerBreaking
- Modules instead of hooksBreaking
- Public SDK surface reducedBreaking
- Other SDK usage
- React and Vue SDK usage
- Migration checklist
Detailed guide for migrating existing Featurevisor projects (using Featurevisor CLI) and applications (using Featurevisor SDKs) to latest v3.0.
Migrate in two steps#
You do not have to migrate everything at once. The recommended path is linear, with the project first and the applications after:
- Upgrade your Featurevisor project: update the CLI, adjust your configuration and definitions, build the datafiles, and deploy them.
- Upgrade your applications: once the new datafiles are published, update each application's SDK at your own pace.
The datafile shape has not changed#
The generated datafile keeps the same schema version (2) as v2, so its shape is unchanged. No new functionalities have been introduced either that would break existing applications.
This means the datafile structure remains readable by applications using existing v2 SDKs.
Behaviour changes#
There is one behavioural exception to review before publishing v3 datafiles: segments.not now uses the same implicit AND semantics as conditions.
Projects without segments.not expressions that have multiple children can finish step 1 and publish v3 datafiles while applications continue using v2 SDKs. There is no need to coordinate a single big release across all applications.
Once the datafiles are out, you can come back and upgrade the SDK in each application one at a time.
Project configuration#
Environments are off by default Breaking#
In v2, every project had staging and production environments by default. In v3, projects have no environments unless you declare them.
If your project uses environments, declare them explicitly in your configuration:
module.exports = { // staging and production // were assumed by default tags: ['all'],}module.exports = { environments: [ 'staging', 'production', ], tags: ['all'],}If your project does not need environments, you can leave environments out, and author your rules, force, and expose directly without an environment key.
The old environments: false value is no longer needed. Remove the property instead.
Learn more in Environments page.
Split environment files removed Breaking#
The splitByEnvironment option and the top-level environments/ definition directory have been removed. Environment-specific rules, force, and expose now belong in their feature file, keyed by environment.
features/ checkout.ymlenvironments/ staging/ checkout.yml production/ checkout.ymldescription: CheckoutbucketBy: userIdrules: staging: - key: everyone segments: '*' percentage: 100 production: - key: everyone segments: '*' percentage: 0Remove splitByEnvironment from featurevisor.config.js after moving the files. If those directories represented independently promoted copies rather than environment-specific rollout fields, consider migrating them to Sets instead.
Scopes replaced by targets Breaking#
The scopes configuration key has been removed. Datafiles are now built from targets, which live as files in the targets directory.
module.exports = { scopes: [ { name: 'browsers', tag: 'web', context: { platform: 'web' }, }, ],}description: Web browserstag: webcontext: platform: webA target's tag, tags, and context work exactly like a scope did, including the or and and selectors. If scopes is still present in your configuration, Featurevisor ignores it. Migrate each old scope to a target file, because obsolete scopes entries no longer build any datafiles.
Learn more in Targets and Scopes pages.
Namespace separator Breaking#
Namespaced feature and segment keys now use a dot (.) as their separator by default, instead of a slash (/).
This means features/checkout/feature1.yml is now referred to as checkout.feature1 instead of checkout/feature1, both in your application and in your test specs.
To keep the old slash-separated keys, set namespaceCharacter in your configuration:
module.exports = { namespaceCharacter: '/',}Learn more in Namespaces page.
Sets New#
You can now split a project into independent trees called sets, each owning its own attributes, segments, features, targets, and tests.
module.exports = { sets: true, tags: ['all'],}This is useful for modeling release lanes like dev, staging, and production, or distinct surfaces like storefront and admin, from a single repository.
Learn more in Sets page.
Promotions New#
In a project with sets, you can copy definitions from one set to another using promotions. You can optionally constrain which promotions are allowed with promotionFlows:
module.exports = { sets: true, tags: ['all'], promotionFlows: [ { from: 'dev', to: 'staging' }, { from: 'staging', to: 'production' }, ],}Learn more in Promotions page.
Defining features#
Tags are now optional New#
The tags property on a feature is now optional. A feature without tags is still included in targets that do not narrow down by tag.
description: My featurebucketBy: userIdrules: - key: everyone segments: '*' percentage: 100Learn more in Features section.
not uses implicit AND semantics Breaking#
For both conditions and rule segments, direct children of not are treated as an implicit AND. In v3, segments.not is aligned with the condition behavior.
Use a nested or when the intention is "none of these match":
rules: - key: neither-premium-nor-internal segments: not: - or: - premium - internal percentage: 100Use a nested and when the intention is "not all of these match":
rules: - key: not-both-premium-and-internal segments: not: - and: - premium - internal percentage: 100Keep v2 and v3 SDKs compatible
During a gradual SDK rollout, avoid placing multiple segment names directly under not. Put one explicit or or and group under it instead.
Both SDK versions then see a single direct child and negate the same grouped result:
not: [{ or: [...] }]means none of the listed segments match.not: [{ and: [...] }]means not all of the listed segments match.
For an existing v2 rule with several direct children under not, wrap those children in or before publishing a datafile that will be consumed by both v2 and v3 applications. This preserves the v2 "none match" behavior throughout the transition. After every application uses a v3 SDK, the explicit grouping can remain because it is clearer and behaves identically.
Empty and, or, and not arrays are now rejected during linting. Update any generated or placeholder expressions before upgrading.
Promotable definitions New#
In a project with sets, you can protect an existing destination definition from later promotion updates by setting promotable: false. A missing destination definition is still created. On rules, a source rule with this field is omitted and an existing destination rule is preserved. This is supported on features, rules, segments, attributes, groups, schemas, targets, and test specs.
description: My featurepromotable: false# ...Learn more in Promotions page.
Testing features#
Targeting datafiles in assertions Breaking#
The scope and tag properties in test assertions have been replaced by a single target property.
feature: myFeatureassertions: - environment: production scope: browsers at: 50 context: {} expectedToBeEnabled: truefeature: myFeatureassertions: - environment: production target: browsers at: 50 context: {} expectedToBeEnabled: trueThe --with-scopes and --with-tags CLI flags are no longer needed. The test runner builds target datafiles in memory automatically. Old scripts may still pass these flags, but they are ignored.
Learn more in Testing page.
Testing sets New#
In a project with sets, tests run for every set by default. You can run tests for a single set with --set:
$ npx featurevisor test --set=storefrontCLI usage#
Upgrade to latest CLI New#
In your Featurevisor project repository:
$ npm install --save @featurevisor/cli@3Building datafiles from targets Breaking#
Datafiles are no longer built per tag automatically. You now need at least one target for the build to produce anything.
The smallest possible target includes all of your non-archived features:
description: All featuresThen build as usual:
$ npx featurevisor buildDatafile naming Breaking#
Because datafiles are built from targets now, the tag- prefix in datafile names is gone. A datafile is named after the target that produced it.
$ tree datafiles.├── production│ └── featurevisor-tag-web.json└── staging └── featurevisor-tag-web.json$ tree datafiles.├── production│ └── featurevisor-web.json└── staging └── featurevisor-web.jsonDefine a target named web (with tag: web) to produce featurevisor-web.json.
To maintain backwards compatibility, you can create new targets named tag-web and tag-mobile to produce featurevisor-tag-web.json and featurevisor-tag-mobile.json respectively.
Learn more in Building datafiles page.
Schema version selection removed Breaking#
Featurevisor v3 no longer builds v1-compatible datafiles. Generated datafiles always use schema version 2, and still include the normal schemaVersion: "2" field.
The old --schema-version and --schemaVersion flags are no longer part of active CLI usage. If they are still passed by old scripts, they are ignored and datafiles are still generated as schema version 2.
If you still have an application consuming v1 datafiles, keep building those datafiles with an older Featurevisor v2 CLI until the application can move to a v2-datafile-compatible SDK.
Catalog instead of site Breaking#
The old site package and command have been replaced by the new standalone @featurevisor/catalog package, exposed through the catalog command.
$ npx featurevisor site export$ npx featurevisor site serve$ npx featurevisor catalog export$ npx featurevisor catalog serveThe default export directory changes from out/ to catalog/. Replace siteExportDirectoryPath with catalogDirectoryPath if your project customizes it, and update deployment workflows to publish the new directory. Running npx featurevisor catalog without a subcommand now starts a watched local development session.
Learn more in Catalog page.
Promote command New#
In a project with sets, you can preview and apply promotions between sets:
$ npx featurevisor promote --from=dev --to=staging$ npx featurevisor promote --from=dev --to=staging --apply$ npx featurevisor promote --from=dev --to=staging --target=checkout --applyPromotion filters such as --target, --tag, --includeFeatures, and --excludeFeatures limit the selected features while automatically including the segments they reference.
Learn more in Promotions page.
Per-feature generated modules removed Breaking#
The TypeScript code generator no longer creates individual *Feature.ts namespaces. The --no-individual-features option has also been removed because the function API is now the only generated API.
Run the v3 code generator with the same project-level command, without --no-individual-features:
$ npx featurevisor generate-code \ --language typescript \ --out-dir ./src \ --reactDelete the old generated output before running the v3 generator so stale *Feature.ts files do not remain in your project. Replace namespace imports with the typed shared functions:
import { CheckoutFeature } from '@yourorg/features'const enabled = CheckoutFeature.isEnabled(context)const variation = CheckoutFeature.getVariation(context)const color = CheckoutFeature.getColor(context)import { isEnabled, getVariation, getVariable,} from '@yourorg/features'const enabled = isEnabled('checkout', context)const variation = getVariation('checkout', context)const color = getVariable('checkout', 'color', context)Feature keys, variable keys, variation values, variable values, and context remain type safe through the shared generated functions. Learn more in Code Generation.
Code generation also accepts repeatable --tag and --target options in v3. All supplied selectors form a union, allowing one generated package to cover several target datafiles.
In a project with sets enabled, generate-code requires --set=<set> because one generated package represents one set's definitions.
Target selection is also available on build, test, evaluate, benchmark, assess-distribution, list --features, and info. The option is repeatable where several targets can be processed. Each runtime command evaluates every selected target independently. A normal build writes every selected target datafile, while build --json and build --print accept only one target because they emit one datafile.
Working with a single set New#
Most commands accept a --set flag to scope them to one set:
$ npx featurevisor build --set=storefront$ npx featurevisor test --set=storefrontJavaScript SDK usage#
Upgrade to latest SDK New#
In your application repository:
$ npm install --save @featurevisor/sdk@3Creating an instance Breaking#
createInstance has been renamed to createFeaturevisor.
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ datafile: datafileContent,})import { createFeaturevisor } from '@featurevisor/sdk'const f = createFeaturevisor({ datafile: datafileContent,})Instance type renamed Breaking#
The FeaturevisorInstance type has been renamed to Featurevisor.
import type { FeaturevisorInstance } from '@featurevisor/sdk'let f: FeaturevisorInstanceimport type { Featurevisor } from '@featurevisor/sdk'let f: FeaturevisorOptions type renamed Breaking#
The constructor options type has been renamed from InstanceOptions to FeaturevisorOptions.
import type { InstanceOptions } from '@featurevisor/sdk'const options: InstanceOptions = { datafile: datafileContent,}import type { FeaturevisorOptions } from '@featurevisor/sdk'const options: FeaturevisorOptions = { datafile: datafileContent,}Learn more in JavaScript SDK page.
setDatafile merges by default Soft breaking#
In v2, calling setDatafile replaced the instance's datafile. In v3, it merges with the existing datafile by default.
Incoming features and segments override matching keys, while existing ones that are missing from the incoming datafile are kept. To get the old replacing behavior, pass true as the second argument.
// always replacedf.setDatafile(datafileContent)// merges by defaultf.setDatafile(datafileContent)// pass true to replace entirelyf.setDatafile(datafileContent, true)Merging makes it possible to load smaller datafiles on demand. Learn more in Loading datafiles on demand.
Sticky values belong to instances Breaking#
The per-evaluation sticky override has been removed. Set sticky values on the main instance, or give a child instance its own sticky values when spawning it.
const enabled = f.isEnabled('checkout', context, { sticky: stickyFeatures,})f.setSticky(stickyFeatures)const enabled = f.isEnabled('checkout', context)For isolated state, use f.spawn(context, { sticky: stickyFeatures }) and evaluate through the returned child instance.
Diagnostics instead of logger Breaking#
The logger option and the createLogger function have been removed. The SDK now reports diagnostics, which you control with logLevel and an optional onDiagnostic handler.
import { createInstance, createLogger,} from '@featurevisor/sdk'const f = createInstance({ logger: createLogger({ level: 'debug', }),})import { createFeaturevisor } from '@featurevisor/sdk'const f = createFeaturevisor({ logLevel: 'debug',})You can pass your own handler if you do not want diagnostics printed to the console:
const f = createFeaturevisor({ logLevel: 'info', onDiagnostic: function (diagnostic) { // send to your observability system },})The setLogLevel method still works for changing the level at runtime.
Learn more in Diagnostics section.
Modules instead of hooks Breaking#
The hooks API has been replaced by the modules API. The Hook type is now FeaturevisorModule, hooks is now modules, and addHook is now addModule.
Your existing before, after, bucketKey, and bucketValue callbacks carry over unchanged. Modules also add an optional setup lifecycle and a close callback.
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ hooks: [myCustomHook],})const removeHook = f.addHook(myCustomHook)import { createFeaturevisor } from '@featurevisor/sdk'const f = createFeaturevisor({ modules: [myCustomModule],})const removeModule = f.addModule(myCustomModule)await removeModule?.()await f.removeModule('my-custom-module')The old interceptContext, configureBucketKey, and configureBucketValue options, which were already replaced by hooks in v2, are now expressed as modules. Module cleanup can be asynchronous, so the function returned by addModule and removeModule should be awaited.
Learn more in Modules section.
Public SDK surface reduced Breaking#
The package root now focuses on createFeaturevisor, the Featurevisor type, child instances, evaluations, events, diagnostics, modules, and Featurevisor data types.
DatafileReader, logger and emitter implementations, evaluator dependency objects, and other internal helpers are no longer public APIs. If application code used DatafileReader to inspect the active datafile, use the corresponding instance methods instead:
| Removed usage | Instance replacement |
|---|---|
reader.getRevision() | f.getRevision() |
reader.getSchemaVersion() | f.getSchemaVersion() |
reader.getSegment(key) | f.getSegment(key) |
reader.getFeature(key) | f.getFeature(key) |
reader.getFeatureKeys() | f.getFeatureKeys() |
reader.getVariableKeys(featureKey) | f.getVariableKeys(featureKey) |
reader.hasVariations(featureKey) | f.hasVariations(featureKey) |
Remove direct imports of undocumented logger, emitter, reader, and evaluator helpers. Use diagnostics, events, and the main instance API instead.
The v1-only DatafileContentV1, FeatureV1, VariationV1, and VariableV1 types have also been removed. Use the normal v2 datafile types. VariableValue no longer includes undefined; missing evaluated values use null.
Other SDK usage#
The v3 SDKs use the same Featurevisor focused factory naming as the JavaScript SDK. The old instance factory names are removed as a clean breaking change.
| SDK | v2 factory | v3 factory |
|---|---|---|
| Go | NewFeaturevisor | CreateFeaturevisor |
| Swift | createInstance | createFeaturevisor |
| Java | Featurevisor.createInstance | Featurevisor.createFeaturevisor |
| Ruby | Featurevisor.create_instance | Featurevisor.create_featurevisor |
| Python | create_instance | create_featurevisor |
| PHP | Featurevisor::createInstance | Featurevisor::createFeaturevisor |
Go now uses FeaturevisorOptions instead of Options. Java now uses Featurevisor.FeaturevisorOptions instead of Featurevisor.Options. Swift and Python expose the main instance as Featurevisor instead of FeaturevisorInstance.
Update imports, type annotations, factory calls, and options type names together when upgrading an application. See the relevant SDK guide for language specific examples.
Custom logger injection has also been removed from all v3 SDKs. Configure diagnostic verbosity with logLevel, update it with setLogLevel or the idiomatic equivalent, and send structured diagnostics to your observability system with onDiagnostic. Remove uses of createLogger, create_logger, NewLogger, logger handler options, and logger fields in SDK options.
The same runtime changes apply across the v3 SDKs:
- hooks are replaced by modules with
setup,before,bucketKey,bucketValue,after, andcloselifecycle callbacks; setDatafilemerges features and segments by default, with an explicit replace argument for the old behavior;- sticky values are configured on the main or child instance instead of individual evaluations;
- diagnostics replace custom logger injection;
- datafiles use schema version
2only.
The exact method names and callback types follow each language's conventions. Review the relevant SDK guide before upgrading and update code and tests together.
React and Vue SDK usage#
The React and Vue packages now expect the renamed createFeaturevisor function and Featurevisor type. Update any provider, application setup, or utility code that imports the old factory or type.
The React provider still receives an already-created instance:
import { createFeaturevisor } from '@featurevisor/sdk'import { FeaturevisorProvider } from '@featurevisor/react'const f = createFeaturevisor({ datafile: datafileContent })root.render( <FeaturevisorProvider instance={f}> <App /> </FeaturevisorProvider>,)If an older Vue setup still passes datafileUrl, fetch the datafile before creating the instance instead:
import { createInstance } from '@featurevisor/sdk'const f = createInstance({ datafileUrl: DATAFILE_URL,})import { createFeaturevisor } from '@featurevisor/sdk'const datafile = await fetch(DATAFILE_URL) .then(response => response.json())const f = createFeaturevisor({ datafile })The Vue package no longer ships useStatus or activateFeature. To track exposure, register a module that reacts to evaluations in its after callback.
Learn more in React SDK and Vue.js SDK pages.
Migration checklist#
Use this order for each existing project:
- Upgrade
@featurevisor/cliand declare environments explicitly when needed. - Move split environment files back into feature definitions, or model independent trees as sets.
- Convert every scope into a target and add at least one target for building datafiles.
- Decide whether to keep slash-separated namespace keys or migrate application and test references to dots.
- Review multi-child
segments.notexpressions and remove empty logical arrays. - Replace assertion
scopeandtagfields withtarget, then run the complete test suite. - Build and inspect every target and environment datafile before publishing it.
- Update Catalog, promotion, code-generation, and CI commands and output paths used by automation.
- Upgrade each application SDK, update factory and type names, and replace logger and hook integrations.
- Review
setDatafile, sticky ownership, module cleanup, and removed helper imports in application code. - Run application tests against the same v3-generated datafiles that will be deployed.
The project and application upgrades can remain separate deployments as described at the start of this guide.

