Skip to content

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

bash
pnpm add @openfeature/nestjs-sdk @openfeature/server-sdk @lucerna-dev/gates-openfeature @lucerna-dev/gates-node
bash
npm 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:

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

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

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

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

Lucerna Developer Docs