# @lunora/flags flags

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](https://lunora.sh). Configure one OpenFeature provider in `lunora/flags.ts` and Lunora wires `ctx.flags` onto every handler — Cloudflare [Flagship](https://github.com/cloudflare/flagship) is the first-class default, but any OpenFeature provider plugs in unchanged. ## Install ```bash pnpm add @lunora/flags @openfeature/server-sdk # Flagship provider (default): pnpm add @cloudflare/flagship ``` ## Configure ```ts // 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`: ```jsonc { "flagship": [{ "binding": "FLAGS", "app_id": "" }] } ``` HTTP mode (non-binding environments): `flagshipProvider({ appId, accountId, authToken })`. Any OpenFeature provider works in place of Flagship: ```ts 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: ```ts 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() }); ``` ## Evaluate ```ts 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. ## Exports | 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` | ## License [FSL-1.1-Apache-2.0](./LICENSE.md)