@lucerna-dev/gates-openfeature
v0.0.1-alpha.0
Published
[OpenFeature](https://openfeature.dev) server provider for Lucerna Gates. It plugs Gates into any OpenFeature-compatible surface — the `@openfeature/server-sdk` API itself, or higher layers like [Vercel's Flags SDK](https://flags-sdk.dev) through its Open
Readme
@lucerna-dev/gates-openfeature
OpenFeature server provider for Lucerna Gates. It plugs Gates into any OpenFeature-compatible surface — the @openfeature/server-sdk API itself, or higher layers like Vercel's Flags SDK through its OpenFeature adapter. Every evaluation runs remotely against the Gates server-side engine (@lucerna-dev/gates-node in mode: "remote"): no background poller, no exposure queue, nothing to close — stateless, so it fits serverless and edge handlers out of the box.
Install
pnpm add @lucerna-dev/gates-openfeature @lucerna-dev/gates-node @openfeature/server-sdkBoth @lucerna-dev/gates-node and @openfeature/server-sdk are peer dependencies.
Quickstart
import { OpenFeature } from "@openfeature/server-sdk";
import { LucernaProvider } from "@lucerna-dev/gates-openfeature";
await OpenFeature.setProviderAndWait(new LucernaProvider({ serverKey: "ck_srv_YOUR_KEY" }));
const client = OpenFeature.getClient();
const context = { targetingKey: "u_42", plan: "pro" };
const showNewBilling = await client.getBooleanValue("new_billing", false, context); // boolean
const variant = await client.getStringValue("checkout_test", "control", context); // variant nameThe key is the environment's secret server key (ck_srv_…) — it needs the gates:evaluate grant, which scaffolded server keys carry, and it pins the environment. The publishable ck_client_… key is rejected at construction.
How evaluation context maps to Gates
| OpenFeature | Gates |
| --------------------------------------------------- | ----------------------------------------------------------------- |
| targetingKey | userId — the sticky-bucketing unit for rollouts and experiments |
| string / number / boolean attribute | a stringified trait, what targeting rules match against |
| Date attribute | an ISO-8601 trait |
| null / undefined / nested structures and arrays | dropped — Gates traits are a flat string map |
Type mapping
| OpenFeature read | Gates concept | Details |
| ----------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getBooleanValue | feature flag (kill switches folded in) | reason TARGETING_MATCH |
| getStringValue | experiment variant | reason SPLIT with variant set, or your default with reason DEFAULT when the identity is not in the experiment; the exposure is recorded server-side on the same request |
| getNumberValue / getObjectValue | — | Gates has no number/object flags: your default with TYPE_MISMATCH |
With Vercel's Flags SDK
import { flag } from "flags/next";
import { createOpenFeatureAdapter } from "@flags-sdk/openfeature";
import { OpenFeature } from "@openfeature/server-sdk";
import { LucernaProvider } from "@lucerna-dev/gates-openfeature";
const lucernaAdapter = createOpenFeatureAdapter(async () => {
await OpenFeature.setProviderAndWait(
new LucernaProvider({ serverKey: process.env.LUCERNA_SERVER_KEY! }),
);
return OpenFeature.getClient();
});
export const showNewBilling = flag<boolean>({
key: "new_billing_page",
defaultValue: false,
identify: ({ cookies }) => ({ targetingKey: cookies.get("uid")?.value }),
adapter: lucernaAdapter.booleanValue(),
});Options
| Option | Default | What it does |
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| serverKey | — | required; secret key with the gates:evaluate grant |
| baseUrl | https://api.uselucerna.app | override for self-hosted or local development |
| remoteCacheTtlMs | 0 | memoize answers in-memory per flag + identity; kill-switch answers stay capped at 10s (the kill-propagation budget) |
| requestTimeoutMs | 5000 | per-request timeout |
| onError | — | tap for evaluation failures — resolvers themselves never throw |
| fetch | platform fetch | override the transport (tests, custom dispatchers) |
Concurrent identical resolutions always share a single round trip, with or without a cache TTL.
Failure semantics
Resolvers never throw. A failed or misconfigured evaluation resolves the safe Gates default — false for flags (off), your default for experiment variants — and surfaces the underlying error through onError. Wire onError: without it, a bad key serves "everything off" silently forever.
