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,
},
});OpenFeature receives attributes unchanged unless Beignet has an authoritative
value for the same reserved key. For example, tenant.id replaces an
attributes.tenantId value, while attributes.tenantId remains available when
the evaluation context has no tenant.
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, createNextServerLoader } from "@beignet/next";
import { createOpenFeatureFlagsProvider } from "@beignet/provider-flags-openfeature";
import { initialPorts } from "@/infra/port-wiring";
import { openFeatureProvider } from "@/infra/flags/openfeature";
export const getServer = createNextServerLoader(() =>
createNextServer({
ports: initialPorts,
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.