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/featureWrite 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', }); // -> falsecreateFeatures 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'); // -> falseinRollout('cust-0101', 10, 'new-checkout')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
resolveover the whole set - React — the provider and the three hooks
- Build-time planning —
plan()for a static build