OpenFeature-based feature flags for Lunora — ctx.flags, useFlag, and a first-class Cloudflare Flagship provider with any OpenFeature provider pluggable
OpenFeature-based feature flags for Lunora. Configure one
OpenFeature provider in lunora/flags.ts and Lunora wires ctx.flags onto every
handler — Cloudflare Flagship is the
first-class default, but any OpenFeature provider plugs in unchanged.
pnpm add @lunora/flags @openfeature/server-sdk
# Flagship provider (default):
pnpm add @cloudflare/flagship// lunora/flags.ts
import { defineFlags } from "@lunora/flags";
import { flagshipProvider } from "@lunora/flags/providers/flagship";
export default defineFlags({
provider: flagshipProvider({ binding: "FLAGS" }), // Workers binding mode — no HTTP, no token
identify: (auth) => auth.userId ?? undefined, // default targetingKey
});Binding mode reads env.FLAGS; configure it in wrangler.jsonc:
HTTP mode (non-binding environments): flagshipProvider({ appId, accountId, authToken }).
Any OpenFeature provider works in place of Flagship:
export default defineFlags({ provider: (env) => new SomeOpenFeatureProvider(env.SOME_KEY) });Two zero-dependency providers ship in the box for tests, local defaults, and binding-driven flags:
import { memoryProvider } from "@lunora/flags/providers/memory";
import { envProvider } from "@lunora/flags/providers/env";
// Static key → value map (booleans, strings, numbers, JSON objects):
export default defineFlags({ provider: memoryProvider({ "dark-mode": true, "page-size": 25 }) });
// Read from the Worker env — `dark-mode` → `env.FLAG_DARK_MODE` (override with `prefix` / `name`):
export default defineFlags({ provider: envProvider() });export const listPosts = query(async (ctx) => {
if (await ctx.flags.boolean("new-ranking", false)) {
/* ... */
}
const hero = await ctx.flags.string("homepage-hero", "control", { plan: "premium" });
const details = await ctx.flags.details.boolean("new-ranking", false); // value + reason + variant
});ctx.flags never throws: a missing flag, type mismatch, or provider error resolves
with the defaultValue. The default targetingKey (from identify) is merged under
any per-call context, and identical evaluations within one request are memoized.
| Export | Contents | Peer dependency |
|---|---|---|
@lunora/flags |
defineFlags, createFlags, types |
@openfeature/server-sdk |
@lunora/flags/providers/flagship |
flagshipProvider (Cloudflare Flagship) |
@cloudflare/flagship |
@lunora/flags/providers/memory |
memoryProvider (static key → value map) |
— (zero extra deps) |
@lunora/flags/providers/env |
envProvider (reads flags from the Worker env) |
— (zero extra deps) |
@lunora/flags/web |
FlagshipClientProvider (browser escape hatch) |
@openfeature/web-sdk, @cloudflare/flagship |
{ "flagship": [{ "binding": "FLAGS", "app_id": "<your-app-id>" }] }