@blueforge-studio/flags
Feature-flag SDK for the BlueForge federation: typed schema with compiled fail-closed defaults, ETag-revalidating poll client, and a thin React hook entry.
Package Info
Versions
| Version | Published |
|---|---|
| 0.1.0 | 2026-10-11 |
Per-version supply chain
| Version | Sig | SBOM | Att |
|---|---|---|---|
| 0.1.0 |
Click a version to view its details.
Supply chain
Provenance
- Published by
- web:5
- When
- 2026-10-11T06:21:25Z
- sha256 (registry-computed)
- de8bd705a9c807f1100d3b50ccc491da8e26ab94eb17ba48f4e6b4e3fba6eeb6
Observed by the registry at publish.
Claimed by the build
The publisher claimed nothing about its build.
Attachments
- Signature
- none
- SBOM
- none
Dependencies
README
@blueforge-studio/flags
Feature-flag SDK for the BlueForge federation (spec: docs/superpowers/specs/2026-09-30-feature-flags-service-design.md §5).
Server-side: it holds a flags:read key. The only browser-safe entry is @blueforge-studio/flags/client, which never sees a key.
Usage
import { defineFlags, createFlagClient } from '@blueforge-studio/flags'
export const flags = defineFlags({
PAYMENT_PLANS: { type: 'enum', values: ['off', 'trial_only', 'all'], default: 'off', maxStalenessMs: 60_000 },
TRIAL_DAYS: { type: 'number', default: 14, min: 0, max: 60 },
BANNER: { type: 'bool', default: false },
})
const client = createFlagClient({
schema: flags,
url: process.env.FORGE_API_URL!, // hosting-api base URL
key: process.env.FLAGS_READ_KEY!, // flags:read key, server env only
app: 'lq-core',
env: 'prod',
})
await client.ready() // resolves, never rejects (default timeout 2 s)
client.enum('PAYMENT_PLANS') // typed getters: bool / enum / number / string
client.get('TRIAL_DAYS') // { value, source, stale, ageMs }
await client.refresh() // force revalidation, e.g. at checkout
Code against FlagSource<D> (implemented by FlagClient and by test fakes), not the concrete client.
Fail-closed semantics
- Never throws at runtime (network error, 4xx/5xx, non-JSON, bad values, throwing store/telemetry). The only throws are startup misconfiguration:
maxStalenessMs < 2 x pollIntervalMs, and a compiled default that is invalid for its own definition. get()is synchronous and cache-backed; refresh is background polling (30 s +/-20% jitter, ETagIf-None-Match, 304 renews the timestamp).- On failure the last-known-good snapshot is served as
stale: true, source: 'stale-cache'until the staleness ceiling, then the compiled default (source: 'compiled-default'). - Staleness ceiling per flag = the tightest of the flag's
maxStalenessMs, the server's per-flagmaxStalenessSeconds, elsecache.maxStalenessMs(15 min). Money flags:maxStalenessMs: 60_000. - Every served value is re-validated against the compiled definition (type, enum membership, number min/max). Failing values fall back to the default and emit
schema_mismatch. Unknown keys from the server are ignored; missing keys use defaults. - 401/403 counts as an outage (
error: 'auth', backoff 5 min). Other failures back off 1 s to 5 min with jitter; one request in flight. - Rollout is not in v1:
subjectis passed through to the server untouched. - Cache is in-memory by default; pass
cache.store({ load(), save() }, synchronous) for cold-start survival.
Browser: @blueforge-studio/flags/client
import { useFlags } from '@blueforge-studio/flags/client'
const { status, get } = useFlags(flags) // 'loading' | 'ready' | 'fallback'
get('BANNER').value
Reads the app's own GET /api/flags (default endpoint, configurable), never hosting-api. Serve it server-side with
flagsHandler(client, { expose: ['BANNER'] }) (explicit allowlist; never expose protected keys). React is an optional peer dependency. UI hiding is cosmetic; enforce server-side.
Wire types
ResolveResponse, ResolveResponseOk, ResolvedFlag are exported for the server side (GET /api/flags/resolve).