Skip to content

Latest commit

 

History

History
97 lines (69 loc) · 3.73 KB

File metadata and controls

97 lines (69 loc) · 3.73 KB

@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. 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.

Install

pnpm add @lunora/flags @openfeature/server-sdk
# Flagship provider (default):
pnpm add @cloudflare/flagship

Configure

// 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:

{ "flagship": [{ "binding": "FLAGS", "app_id": "<your-app-id>" }] }

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() });

Evaluate

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