@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

Latest Version0.1.0
Licensenone
Keywords-
Repository-
Author-
Homepage-
Bugs-

Versions

VersionPublished
0.1.02026-10-11

Per-version supply chain

VersionSigSBOMAtt
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

Loading dependency graph...

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, ETag If-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-flag maxStalenessSeconds, else cache.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: subject is 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).