@0disoft/openfeature-local-provider
v0.15.0
Published
Local OpenFeature provider for JSON/YAML flag snapshots, CLI validation, file reload/watch, typed evaluation, environment overrides, bucketing, replay fixtures, redacted audit events, and lockable rotating file audit sinks.
Maintainers
Readme
@0disoft/openfeature-local-provider
Local OpenFeature provider for JSON/YAML flag snapshots, typed evaluation, explicit CLI validation, environment overrides, deterministic rollout bucketing, replay fixtures, and redacted audit logs.
Install
npm install @0disoft/openfeature-local-provider @openfeature/server-sdkQuick Start
import { OpenFeature } from "@openfeature/server-sdk";
import { createLocalProvider, parseJsonFlagSnapshot } from "@0disoft/openfeature-local-provider";
const snapshot = parseJsonFlagSnapshot(
JSON.stringify({
schemaVersion: 1,
flags: {
"checkout.enabled": {
type: "boolean",
defaultVariant: "on",
variants: {
on: true,
off: false
}
}
}
})
);
await OpenFeature.setProviderAndWait(createLocalProvider({ snapshot }));
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("checkout.enabled", false);Percentage Rollout
import { OpenFeature } from "@openfeature/server-sdk";
import { createLocalProvider, parseJsonFlagSnapshot } from "@0disoft/openfeature-local-provider";
const snapshot = parseJsonFlagSnapshot(
JSON.stringify({
schemaVersion: 1,
flags: {
"checkout.rollout": {
type: "boolean",
defaultVariant: "off",
variants: {
on: true,
off: false
},
rollout: [
{
variant: "on",
percentage: 25,
seed: "checkout-rollout-v1"
}
]
}
}
})
);
await OpenFeature.setProviderAndWait(createLocalProvider({ snapshot }));
const client = OpenFeature.getClient();
const enabled = await client.getBooleanValue("checkout.rollout", false, {
targetingKey: "account-123"
});Rollout selection is deterministic for the same flag key, targeting key, and seed.
YAML Snapshots
import { OpenFeature } from "@openfeature/server-sdk";
import { createLocalProvider, parseYamlFlagSnapshot } from "@0disoft/openfeature-local-provider";
const snapshot = parseYamlFlagSnapshot(`
schemaVersion: 1
flags:
checkout.enabled:
type: boolean
defaultVariant: "on"
variants:
"on": true
"off": false
`);
await OpenFeature.setProviderAndWait(createLocalProvider({ snapshot }));YAML input must parse into the same schema v1 snapshot contract used by JSON input. Snapshot objects, flag definitions, and rollout rules reject unknown fields; flag names, variant names, and metadata keys remain caller-defined.
CLI Validation
npx -p @0disoft/openfeature-local-provider openfeature-local-provider validate ./flags.yaml
npx -p @0disoft/openfeature-local-provider openfeature-local-provider validate ./flags.json --jsonThe CLI is read-only. It validates local JSON/YAML snapshots, prints a small summary,
and exits with 0 for success, 1 for snapshot validation failure, or 2 for usage
errors.
File Loading And Reload
import { OpenFeature } from "@openfeature/server-sdk";
import {
createReloadableLocalProvider,
loadFlagSnapshotFile,
watchFlagSnapshotFile
} from "@0disoft/openfeature-local-provider";
const path = new URL("./flags.yaml", import.meta.url);
const provider = createReloadableLocalProvider({
snapshot: await loadFlagSnapshotFile(path)
});
await OpenFeature.setProviderAndWait(provider);
const watcher = await watchFlagSnapshotFile({
path,
onSnapshot(snapshot) {
provider.updateSnapshot(snapshot);
},
onError(error) {
console.warn("Flag reload failed", error);
}
});loadFlagSnapshotFile supports .json, .yaml, and .yml files by extension.
Snapshot files are limited to 10 MiB by default; pass maxBytes when a local process
needs a different limit. The loader uses a bounded read from one open file handle, so a
replacement or file growth cannot bypass that limit. Watcher reload failures are reported through onError and do
not replace the last valid snapshot. Evaluation never reads from disk on the flag
resolution path. macOS combines native file and directory watchers so direct writes and
atomic replacements use their strongest event source, and rearms direct file watching after
replacement. Windows file watching uses path polling internally to avoid unstable native
file-event behavior on Node.js 24. Native watcher and reload errors are reported through
onError. Event-driven reloads suppress callbacks for an unchanged parsed snapshot; explicit
reload() calls still invoke onSnapshot after successful validation.
Call watcher.close() during process shutdown when the file watcher is no longer
needed.
Environment Overrides
import { OpenFeature } from "@openfeature/server-sdk";
import { createLocalProvider, parseJsonFlagSnapshot } from "@0disoft/openfeature-local-provider";
const snapshot = parseJsonFlagSnapshot(
JSON.stringify({
schemaVersion: 1,
flags: {
"checkout.enabled": {
type: "boolean",
envVar: "CHECKOUT_ENABLED",
defaultVariant: "on",
variants: {
on: true,
off: false
}
}
}
})
);
await OpenFeature.setProviderAndWait(
createLocalProvider({
snapshot,
env: {
CHECKOUT_ENABLED: "false"
}
})
);Environment variables are explicit. The provider does not invent variable names from flag
keys. Explicit JSON overrides are limited to 10 MiB by default; pass
maxOverridesJsonBytes when creating overrides or providers that need a different local
limit.
File Audit Sink
import { join } from "node:path";
import {
createFileAuditSink,
createLocalProvider,
parseJsonFlagSnapshot
} from "@0disoft/openfeature-local-provider";
const snapshot = parseJsonFlagSnapshot(snapshotJson);
const auditSink = createFileAuditSink({
path: join(process.cwd(), "var", "audit", "openfeature.jsonl"),
maxBytes: 10 * 1024 * 1024,
maxFiles: 5,
maxQueueSize: 1000,
queueOverflowPolicy: "reject",
lock: true
});
const provider = createLocalProvider({
snapshot,
auditSink,
auditRedaction: {
contextKeys: "none"
}
});
await auditSink.flush?.();Audit events are redacted before they reach the sink. Provider audit writes are non-blocking by default, and file write failures are reported through the OpenFeature logger without changing the evaluated flag value.
New audit directories and files use private 0700 and 0600 modes on POSIX systems.
The final audit path must be a regular, current-user-owned file with one hard link and
must not be a symbolic link. Keep the whole path inside an application-owned directory;
on Windows, enforce this with directory ACLs because no-follow open is not available.
Context keys are counted by default without including their names. Set
auditRedaction.contextKeys to "names" only for fixed schema-like keys when names are
required, or to "none" to omit both names and count. These modes never enable raw
context values; targetingKeyPresent remains a boolean presence signal.
Use auditSink.flush?.() before process exit when a short-lived script must wait for
pending non-blocking audit writes. Use auditWriteMode: "blocking" when each
evaluation promise must wait for its audit write. OpenFeature.close() invokes the
provider close hook, which flushes the configured sink without taking ownership of or
closing a sink that another provider may share.
The queue is bounded to 5,000 pending writes by default. Set a positive maxQueueSize to
choose another limit, or set maxQueueSize: null only when preserving the pre-0.15
unbounded behavior is worth the process-memory risk. The default overflow policy is
reject; set queueOverflowPolicy: "dropNewest" to resolve the overflowing write without
appending that event. auditSink.getStats?.() returns pending, dropped, and rejected
write counters plus the effective queue limit. Counters are cumulative for the sink
lifetime.
Set maxBytes to rotate the active audit file by size. maxFiles controls how many
rotated files are retained as .1, .2, and so on.
Treat the audit file path as trusted local configuration. Do not pass tenant, request, or
unvalidated user input directly into path.
Set lock: true when multiple local processes may write the same audit file. The lock is
advisory and uses a sibling .lock file. lockTimeoutMs controls acquisition timeout,
and staleLockMs can remove stale lock files left by crashed processes. Lock ownership
is checked before release so an old writer does not remove a replacement owner's lock.
This is best-effort local-filesystem coordination, not a distributed lock; an aggressive
stale timeout can still allow concurrent live writers.
Multiple sinks loaded from the same package instance are serialized by audit path within
one process. Separate processes or duplicate installed package instances still require
lock: true when they share a rotating audit path.
Supported Runtime
- Node.js 22 LTS
- Node.js 24 LTS
Browser, Bun, Deno, hosted flag services, control-plane APIs, and remote streaming are outside the current package boundary.
