NestJS
NestJS needs no Lucerna-specific integration: @openfeature/nestjs-sdk is a Nest module over the same @openfeature/server-sdk the Lucerna provider implements. Register the provider once in the module, then use Nest's decorators everywhere.
The provider is stateless — no lifecycle, no background poller — so there is nothing for Nest's DI container to initialize or tear down. It works the same under Express, Fastify, and serverless Nest.
1. Install
pnpm add @openfeature/nestjs-sdk @openfeature/server-sdk @lucerna-dev/gates-openfeature @lucerna-dev/gates-nodenpm install @openfeature/nestjs-sdk @openfeature/server-sdk @lucerna-dev/gates-openfeature @lucerna-dev/gates-node@openfeature/nestjs-sdk also expects @nestjs/common, @nestjs/core, and rxjs — already present in every Nest app.
2. Register the module
OpenFeatureModule.forRoot takes the provider, plus an optional contextFactory that builds the evaluation context from the request — so decorator reads are targeted per user without passing context by hand:
import { Module } from "@nestjs/common";
import { OpenFeatureModule } from "@openfeature/nestjs-sdk";
import { LucernaProvider } from "@lucerna-dev/gates-openfeature";
@Module({
imports: [
OpenFeatureModule.forRoot({
defaultProvider: new LucernaProvider({
serverKey: process.env.LUCERNA_SERVER_KEY!,
onError: (error) => console.error("[gates]", error.message),
}),
contextFactory: (context) => {
const { user } = context.switchToHttp().getRequest();
return { targetingKey: user?.id, plan: user?.plan };
},
}),
],
})
export class AppModule {}targetingKey becomes the Gates userId; other attributes become traits your targeting rules match on. Wire onError — reads never throw, so without it a misconfigured key serves safe defaults ("everything off") silently.
3. Read flags
In route handlers
The flag decorators inject the evaluation as an RxJS observable of evaluation details:
import { Controller, Get } from "@nestjs/common";
import { BooleanFeatureFlag } from "@openfeature/nestjs-sdk";
import { map, type Observable } from "rxjs";
import type { EvaluationDetails } from "@openfeature/server-sdk";
@Controller("billing")
export class BillingController {
@Get()
page(
@BooleanFeatureFlag({ flagKey: "new_billing", defaultValue: false })
newBilling: Observable<EvaluationDetails<boolean>>,
) {
return newBilling.pipe(map(({ value }) => (value ? "new" : "classic")));
}
}Boolean reads answer feature flags with kill switches folded in. For experiments, @StringFeatureFlag injects the assigned variant name — and the exposure is recorded server-side on the read.
In services
Inject the OpenFeature client where a decorator doesn't fit; here the evaluation context is passed explicitly:
import { Injectable } from "@nestjs/common";
import { OpenFeatureClient } from "@openfeature/nestjs-sdk";
import type { Client } from "@openfeature/server-sdk";
@Injectable()
export class CheckoutService {
constructor(@OpenFeatureClient() private readonly client: Client) {}
async variant(userId: string): Promise<string> {
return this.client.getStringValue("checkout_test", "control", { targetingKey: userId });
}
}4. Guard routes on a flag
@RequireFlagsEnabled turns a flag into an access gate — the handler only runs while every listed flag is on for the request's context:
import { RequireFlagsEnabled } from "@openfeature/nestjs-sdk";
@RequireFlagsEnabled({ flags: [{ flagKey: "beta_api" }] })
@Get("beta")
beta() {
return this.service.beta();
}Gating is not authorization
A flag decides who sees a feature, not who is allowed to use it. Keep permission checks in your guards and services — a route flipped on by a rollout is still reachable by anyone matching the rule.
Next
- Reference — provider options, context mapping, failure semantics, request caching.
- Vercel Flags SDK — the same provider inside Next.js
flag()declarations.