These pages document @evanion/feature 0.1.1, the version on npm.
Many Processes
To ensure a consistent user experience across a distributed system, every process must agree on which feature variant a user sees. In a game shop architecture, this is achieved by separating the authoring of configuration from its consumption across three processes: shop-api, the storefront, and the back office.
One process (shop-api) writes the configuration document using serializeConfig, while the others install it via parseFeatureConfig and features.reload. Because they all resolve the same shopper to the same variant, the user experience remains seamless regardless of which process handles the request.
How shop-api Writes the Document
To publish feature flags, @evanion/feature allows a central process like shop-api to author definitions and serialize them for others. shop-api holds the authored definitions, such as new-checkout and express-pickup, which depends on it. It builds a store from them with createFeatures, and writes the store out with serializeConfig. The version option names the document, and maxStale records how long shop-api thinks a holder may keep it.
import { , } from '@evanion/feature';
const = ([
{ : 'new-checkout', : true },
{ : 'express-pickup', : true, : ['new-checkout'] },
]);
const = (, {
: '2026-10-08.1',
: 60_000,
});
.; // -> '2026-10-08.1'
.; // -> 60000
..; // -> 2
.; // -> undefinedTo ensure the document is not corrupted during transit, shop-api stamps it with a digest. Because serializeConfig leaves digest unset, shop-api calls configDigest on the document and writes the result into the digest member. This digest consists of 32 hex characters computed over the document’s text. It excludes version and digest so that relabelling a document does not change its identity, and a publisher can compute the digest before writing it into the document. Every other member, including maxStale, is included in the digest.
import {
,
,
,
} from '@evanion/feature';
const = ([{ : 'new-checkout', : true }]);
const = (, { : 41 });
const = ();
.; // -> 32
// `version` is outside the text, so a relabelled document digests the same.
({ ..., : 42 }) === ; // -> true
// Every other member is inside it.
({ ..., : 60_000 }) === ; // -> falseHow the Storefront and the Back Office Parse It
Once the configuration document reaches the storefront or back office as JSON, these processes must install it to begin resolving variants. A process hands the parsed value to parseFeatureConfig, which checks every member and reports every defect it finds, returning a store only when the document is valid. Both consumers call it the same way because neither holds any definition of its own.
import { } from '@evanion/feature';
import type { } from '@evanion/feature';
const : = {
: '2026-10-08.1',
: [
{ : 'new-checkout', : true },
{ : 'express-pickup', : true, : ['new-checkout'] },
],
};
const = ();
.; // -> true
. && ..; // -> '2026-10-08.1'
. && ..('express-pickup'); // -> trueThe resulting store retains the document’s version and the rest of its envelope. A process that needs to log which document it is currently serving can use features.version to get the version the store was built from, and features.envelope to retrieve the schemaVersion and maxStale.
import { } from '@evanion/feature';
const = ({
: 'flags@41',
: '2026-10-01',
: 60_000,
: [
{ : 'new-checkout', : true },
{ : 'express-pickup', : true, : ['new-checkout'] },
],
});
.; // -> 'flags@41'
..; // -> '2026-10-01'
..; // -> 60000
.('express-pickup'); // -> trueHow a Running Process Reloads
Because processes are long-lived, they must be able to update their configuration without restarting. If the back office started on yesterday’s document and is still running, it calls features.reload when shop-api serves a new one.
A reload validates the document using the same logic as parseFeatureConfig and installs it only upon success. The result names the new version, the previousVersion, and the keys in changed. If a document fails validation, the previous configuration continues to decide variants.
import { } from '@evanion/feature';
const = ({
: 41,
: [
{ : 'new-checkout', : true },
{ : 'express-pickup', : true, : ['new-checkout'] },
],
});
const = .({
: 42,
: [
{ : 'new-checkout', : true },
{ : 'express-pickup', : false, : ['new-checkout'] },
],
});
; // -> { ok: true, version: 42, previousVersion: 41, changed: ['express-pickup'] }
.('express-pickup'); // -> falseWhy the Three Processes Agree
Consistency is achieved because each process holds the same rules and computes its own answer independently. No process asks another which variant a shopper gets. Because the bucket depends only on the shopper’s targetingKey and the feature’s seed, the same document gives the same answer in every process.
The digest is the mechanism used to verify that documents match across the network. Since it survives a JSON hop, two processes that fetch the document separately can compute a digest to confirm they hold the same configuration.
import {
,
,
,
} from '@evanion/feature';
import type { } from '@evanion/feature';
const = ([{ : 'new-checkout', : true }]);
const = (, { : 41 });
const = .(.()) as ;
() === (); // -> trueIf a holder finds a digest on a document, it recomputes it. If the two values disagree, the document is refused with digest-mismatch.
import {
,
,
,
,
} from '@evanion/feature';
const = ([{ : 'new-checkout', : true }]);
const = (, { : 41 });
const = { ..., : () };
(); // -> { ok: true }
const = { ..., : 60_000 };
const = ();
. ? [] : ..(() => .); // -> ['digest-mismatch']All Three in One Fence
The fence below demonstrates the complete lifecycle: shop-api serializes and stamps the digest, the storefront boots from the resulting JSON, and the back office boots from a previous document before reloading this new one. Finally, the fence asserts that all three processes resolve the same targetingKey to the same variant.
import {
,
,
,
,
} from '@evanion/feature';
import type { } from '@evanion/feature';
// shop-api holds the authored definitions and writes the document once.
const = ([
{ : 'new-checkout', : true },
{
: 'checkout-cta',
: true,
: ['new-checkout'],
: [
{ : 'control', : 50 },
{ : 'blue', : 50, : { : 'Get it' } },
],
},
]);
const = (, {
: '2026-10-08.1',
: 60_000,
});
const = .({ ..., : () });
// The storefront boots from the document.
const = (.() as );
// The back office booted on yesterday's document and reloads today's.
const : = {
: '2026-10-07.4',
: [{ : 'new-checkout', : false }],
};
const = ();
if (!. || !.) throw new ('document refused');
const = ..(.() as );
.; // -> true
. && .; // -> '2026-10-07.4'
..; // -> '2026-10-08.1'
..; // -> '2026-10-08.1'
...; // -> 60000
const = { : 'cust-0042' };
const = .('checkout-cta', );
; // -> 'control'
..('checkout-cta', ) === ; // -> true
..('checkout-cta', ) === ; // -> trueWhat You Supply
@evanion/feature evaluates the document it is handed but does not handle the transport. You must supply the source that carries the bytes from shop-api to the other processes, such as an HTTP endpoint, a file, or a queue. The library’s responsibility ends at parseFeatureConfig and features.reload. Issue #423 draws the line for the adapter package, and the Limits page of this section lists what the library does not do.
Distribution carries the reference for the document’s members, the schema, and maxStale.