Skip to Content
FeatureGetting Started

Getting started

Not on npm yet

npm install @evanion/feature does not resolve. The package is private: true, so a release run versions and tags it without publishing: npm cannot configure a trusted publisher for a package that does not exist on the registry, and the first version has to go up by hand.

The examples on this page are regions of libs/feature/README.md and run under nx test @evanion/feature.

Baize is rolling out a new checkout, and express pickup is an option that only exists inside it. This page writes both down, resolves them for one customer, and turns the parent off.

npm install @evanion/feature

Write the flags down

A feature is stored intent: enabled is what the shop wants, and rules decide whether that intent reaches a given customer. dependsOn names the flags that have to resolve on first.

import { } from '@evanion/feature'; const = ([ { : 'new-checkout', : true, : [ { : 'after-launch', : [{ : 'now', : 'after', : '2026-10-01T00:00:00Z' }], : { : 25, : 'targetingKey' }, }, ], }, { : 'express-pickup', : true, : ['new-checkout'] }, ]); const = new ('2026-11-01T00:00:00Z'); .('express-pickup', { : 'cust-0042', }); // -> true .('express-pickup', { : 'cust-0107', }); // -> false

createFeatures validates the graph and returns a store. isEnabled resolves one flag for one context: targetingKey is the customer the rollout buckets on, and now is the instant the time window is read against.

cust-0042 and cust-0107 get different answers because the 25% rollout put them in different buckets. Neither answer moves on the next request.

Ask which tenth a customer is in

The rollout is a hash rather than a draw, so one customer’s answer is the same everywhere and forever. That is the property a shop needs and a coin flip does not have.

import { } from '@evanion/feature'; ('cust-0101', 10, 'new-checkout'); // -> true ('cust-0042', 10, 'new-checkout'); // -> false
inRollout('cust-0101', 10, 'new-checkout')

true

Retype the id. The same customer always lands in the same place, and about one in ten is in.

Retype the id. Roughly one id in ten is in, a neighbouring id lands anywhere, and the same id always comes back the same. Rollouts has the bucket behind it and what happens when the percentage moves.

Read why a flag is off

isEnabled answers with a boolean. resolve answers with one decision per flag, and the decision explains itself: reason says which of the five cases this is, and dependency-off carries the parent that blocked it.

import { } from '@evanion/feature'; const = ([ { : 'new-checkout', : true, : [ { : 'staff-first', : [{ : 'role', : 'eq', : 'bookseller' }], }, ], }, { : 'express-pickup', : true, : ['new-checkout'] }, ]); .({ : 'customer' })['express-pickup']; // -> { key: 'express-pickup', enabled: false, reason: 'dependency-off', blockedBy: 'new-checkout', cause: { key: 'new-checkout', reason: 'no-rule-matched', rule: 'staff-first' } }

Here the new checkout is on for booksellers first, so for a customer it never matched a rule — and express pickup is off with blockedBy naming the flag to look at. Decisions has the other four reasons.

Turn the parent off

toggle is the only call that writes. It sets enabled on the stored definition and reports which dependants go off with it.

import { } from '@evanion/feature'; const = ([ { : 'new-checkout', : true }, { : 'express-pickup', : true, : ['new-checkout'] }, { : 'demo-night-booking', : true, : ['express-pickup'] }, ]); .('new-checkout', false); // -> { ok: true, key: 'new-checkout', enabled: false, willDisable: ['express-pickup', 'demo-night-booking'] }

willDisable is transitive and in dependency order: demo night booking is behind express pickup, which is behind the new checkout. A confirmation dialog can show that list; a script can ignore it.

Nothing else writes. resolve and plan are pure functions of the configuration, the context and the clock, so a window closing changes an answer without changing anything stored.

Where to go next

  • Configuration — every field of a definition, and the precedence between them
  • Rollouts — the bucket, the seed, and why raising a percentage only adds members
  • Decisions — the five reasons, and resolve over the whole set
  • React — the provider and the three hooks
  • Build-time planning — plan() for a static build
Last updated on