Feature flags
Feature flags let production apps ship changes behind typed runtime decisions
without coupling use cases to a vendor SDK. Beignet owns the flag definitions
and FlagsPort; providers decide where evaluation happens.
The API follows OpenFeature's core semantics — typed values, targeting context, defaults, evaluation details, and tracking — while keeping imports and tests in Beignet's port/provider model.
Define flags
Declare flags in the feature that owns the behavior:
// features/billing/flags.ts
import { defineFlag, defineFlags } from "@beignet/core/flags";
export const billingFlags = defineFlags({
newCheckout: defineFlag.boolean("billing.new-checkout", {
default: false,
description: "Route checkout creation through the new billing flow.",
}),
pricingCopy: defineFlag.string("billing.pricing-copy", {
default: "control",
}),
});String and number flags widen to string and number by default. Pass a
generic when the app wants a closed set of variants, such as
defineFlag.string<"control" | "treatment">(...).
Evaluate in application code
Use cases evaluate flags through ctx.ports.flags. Provider failures return the
flag default instead of throwing into product workflows; call details(...)
when you need the reason, variant, metadata, or error summary.
const enabled = await ctx.ports.flags.evaluate(billingFlags.newCheckout, {
context: {
targetingKey: requireUserId(ctx),
subject: { type: "user", id: requireUserId(ctx) },
tenant: ctx.tenant,
attributes: { plan: account.plan },
privateAttributes: ["email"],
requestId: ctx.requestId,
traceId: ctx.traceId,
},
});Plain evaluation does not record exposure. Record exposure explicitly when the user actually sees or can be affected by the flagged behavior:
await ctx.ports.flags.recordExposure(billingFlags.newCheckout, {
context: flagContext,
value: enabled,
});Setup with OpenFeature
Use the OpenFeature provider to adapt LaunchDarkly, Statsig, Unleash, flagd, or
any other OpenFeature-compatible provider behind Beignet's FlagsPort:
bun add @beignet/provider-flags-openfeature @openfeature/server-sdkimport { createNextServer } from "@beignet/next";
import { createOpenFeatureFlagsProvider } from "@beignet/provider-flags-openfeature";
import { appPorts } from "@/infra/app-ports";
import { openFeatureProvider } from "@/infra/flags/openfeature";
export const server = await createNextServer({
ports: appPorts,
providers: [
createOpenFeatureFlagsProvider({
provider: openFeatureProvider,
domain: "app",
}),
],
context: appContextBlueprint,
});The provider contributes ctx.ports.flags and ctx.ports.openFeature as an
escape hatch for advanced OpenFeature operations.
Testing
Use createMemoryFlags(...) or createStaticFlags(...) for tests:
import { createMemoryFlags } from "@beignet/core/flags";
import { billingFlags } from "@/features/billing/flags";
const flags = createMemoryFlags();
flags.set(billingFlags.newCheckout, true);createTestPorts(...) includes an in-memory flags port by default, so most
use-case tests only need to override the flag values they exercise.
Devtools
When devtools are installed before a flags provider, evaluations, provider errors, explicit exposures, and tracking events appear under the Feature flags watcher. Private attributes are redacted from instrumentation details.