Skip to content

Migrating from LaunchDarkly to OpenFeature in NestJS

NestJS teams accumulate LaunchDarkly flag debt the same way everyone else does — flags ship behind boolVariation calls, the feature rolls out, and the flag stays. Multiply that by a service layer spread across a checkout module, a pricing module, and a promotions module, and a year later you have dozens of live flag keys, a handful of stale ones, and no clear picture of how long a migration plan would actually take.

The specific challenge with OpenFeature NestJS migrations is that the LaunchDarkly SDK can live in three or four different services simultaneously. A grep across ./src tells you how many call sites exist, but it conflates flag keys, call types, and staleness signals into one undifferentiated count — giving you no basis for estimating effort or scheduling the work.

This article walks through a complete OpenFeature NestJS migration using FlagLint: audit your flag debt, understand what the tool can safely rewrite versus what needs manual review, set up the OpenFeature provider, and enforce the boundary in CI.


Start with a dry measurement of what you’re working with. Run the audit against your service source, excluding tests so test fixtures don’t inflate your counts:

Terminal window
npx flaglint@latest audit ./src --exclude-tests

Real output from a three-module NestJS service:

- Auditing ./src...
# FlagLint Audit Report
**Scanned at:** 2026-08-13T03:07:33.316Z
**Scan root:** ./src
**Files scanned:** 3
**Duration:** 41ms
## Summary
| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 7 | 2 | 5 | 7 |
| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|-------------------|---------------|
| 0 | 1 | 0 | 1 | 6 | 1 |
## Migration Readiness
Migration readiness: **86/100** · ready
[██████████████████████░░░] 86%
6 safely automatable · 1 require manual review
## Flag Debt Inventory
| Flag Key | Risk | Usages | Files | Call Types | Reasons |
|----------|------|--------|-------|------------|---------|
| `promo-discount-amount` | 🔴 High | 1 | 1 | variationDetail | detail evaluation |
| `temp-discount-banner` | 🔴 High | 1 | 1 | boolVariation | stale signal |
| `new-checkout-flow` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| `checkout-discount-rate` | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable |
| `checkout-ui-theme` | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| `promo-codes-enabled` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| `dynamic-pricing-v2` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
## Next Steps
- Run `flaglint migrate --dry-run` to preview safe OpenFeature rewrites
- Run `flaglint validate --no-direct-launchdarkly` to enforce OF boundary in CI
- Review HIGH risk flags manually before any automated migration
✓ Audit complete: 7 flags — 2 high risk, 5 medium risk (41ms, 3 files)
Migration readiness: 86/100 · ready
[██████████████████████░░░] 86%
6 safely automatable · 1 require manual review

The readiness score of 86/100 means the majority of this migration plan can be automated. Two flag keys block a clean score:

  • promo-discount-amount — call type is variationDetail, which returns a reason object alongside the value. OpenFeature has a detail API, but the response shape differs from LaunchDarkly’s. This requires manual review.
  • temp-discount-banner — the flag key contains a staleness signal (temp). FlagLint surfaces this as a warning, not a block: you should verify whether to remove the flag entirely or migrate it.

The five remaining flag keys — boolean, number, and string calls with static flag keys — are safely automatable.


Step 2 — Set up the OpenFeature provider service

Section titled “Step 2 — Set up the OpenFeature provider service”

Before applying any rewrites, wire up the OpenFeature provider in NestJS. The key point: you keep your LaunchDarkly SDK. It becomes the OpenFeature provider backend, so your flag keys, targeting rules, and rollout configurations are unchanged in the LaunchDarkly dashboard.

Install the packages:

Terminal window
npm install @openfeature/server-sdk @launchdarkly/openfeature-node-server

Create a shared FeatureFlagsService that your modules inject:

src/platform/feature-flags.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { OpenFeature, Client } from "@openfeature/server-sdk";
import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
@Injectable()
export class FeatureFlagsService implements OnModuleInit {
client!: Client;
async onModuleInit() {
await OpenFeature.setProviderAndWait(
new LaunchDarklyProvider(process.env.LD_SDK_KEY!)
);
this.client = OpenFeature.getClient();
}
}

Export this service from a shared FeatureFlagsModule and import that module wherever flag evaluation happens. Keeping the OpenFeature client in one place means you swap providers later — from LaunchDarkly to Flipt or another backend — without touching your service layer.

Tell FlagLint about the binding so the validate command knows your team’s OpenFeature client is featureFlagsService.client. Add this to .flaglintrc:

{
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
"openFeatureClientBindings": [
{
"importName": "openFeatureClient",
"modulePatterns": ["**/platform/feature-flags.service"]
}
]
}

Run the migration in dry-run mode to see exactly what FlagLint will change before touching any file:

Terminal window
npx flaglint@latest migrate ./src --dry-run --exclude-tests
- Scanning ./src...
LaunchDarkly usages found: 7
Safely automatable: 6 · Manual review: 1
Reviewable diffs: 6
Skipped usages: 1

The generated diffs for the six safely automatable flag keys:

--- a/platform/ld-client.ts
+++ b/platform/ld-client.ts
@@ -7,1 +7,1 @@
- const newCheckout = await ldClient.boolVariation("new-checkout-flow", ctx, false);
+ const newCheckout = await openFeatureClient.getBooleanValue("new-checkout-flow", false, ctx);
@@ -8,1 +8,1 @@
- const discountRate = await ldClient.numberVariation("checkout-discount-rate", ctx, 0);
+ const discountRate = await openFeatureClient.getNumberValue("checkout-discount-rate", 0, ctx);
@@ -9,1 +8,1 @@
- const checkoutTheme = await ldClient.stringVariation("checkout-ui-theme", ctx, "default");
+ const checkoutTheme = await openFeatureClient.getStringValue("checkout-ui-theme", "default", ctx);
@@ -15,1 +15,1 @@
- const promoEnabled = await ldClient.boolVariation("promo-codes-enabled", ctx, false);
+ const promoEnabled = await openFeatureClient.getBooleanValue("promo-codes-enabled", false, ctx);
@@ -23,1 +23,1 @@
- const dynamicPricing = await ldClient.boolVariation("dynamic-pricing-v2", ctx, false);
+ const dynamicPricing = await openFeatureClient.getBooleanValue("dynamic-pricing-v2", false, ctx);
@@ -24,1 +24,1 @@
- const discountFlag = await ldClient.boolVariation("temp-discount-banner", ctx, false);
+ const discountFlag = await openFeatureClient.getBooleanValue("temp-discount-banner", false, ctx);

Notice the argument order flip: LaunchDarkly SDK takes (flagKey, context, fallback) while OpenFeature takes (flagKey, fallback, context). A regex-based migration misses this entirely — it produces syntactically valid code that silently evaluates the wrong flag state in production. FlagLint’s AST scanner applies the correct transposition for each call type. See why argument-order bugs are the most common migration mistake.

The skipped call:

platform/ld-client.ts:17:28 — `promo-discount-amount` via `variationDetail`:
detail methods skipped: OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature
detail result parity requires manual review

For variationDetail and its typed variants, FlagLint marks the call as requiring manual review rather than generating a diff. The OpenFeature equivalent is getBooleanDetails/getStringDetails/etc., but the reason enum values in the response differ from LaunchDarkly’s EvaluationReason. Review these manually before applying. See five patterns that block automatic migration for the complete list.


Once you’ve reviewed the dry-run output, apply it on a branch:

Terminal window
git checkout -b migrate/openfeature
npx flaglint@latest migrate ./src --apply --exclude-tests

Replace the openFeatureClient placeholder in each rewritten call site with your featureFlagsService.client reference from Step 2. The placeholder is intentional: FlagLint knows the OpenFeature client variable name is project-specific and won’t assume it.


Step 5 — Enforce the OpenFeature boundary in CI

Section titled “Step 5 — Enforce the OpenFeature boundary in CI”

The migration is only as durable as your CI gate. Without enforcement, new LaunchDarkly SDK calls can creep back in as team members add features — eroding the OpenFeature NestJS boundary you just established.

Add validation to your GitHub Actions workflow:

- name: Enforce OpenFeature boundary
run: npx flaglint@latest validate ./src --no-direct-launchdarkly --exclude-tests

The validate command exits non-zero if any call to the LaunchDarkly SDK’s direct evaluation methods is found. Combined with the openFeatureClientBindings config from Step 2, FlagLint distinguishes between your shared OpenFeature provider setup (allowed) and new direct LaunchDarkly SDK calls (blocked).

For a complete CI workflow including baseline mode and PR comments, see enforcing your LaunchDarkly to OpenFeature migration in GitHub Actions.


Return to the two flags from the audit that need manual decisions:

promo-discount-amount (variationDetail): Check whether the caller uses the reason field from the detail response. If it does, implement the equivalent using getBooleanDetails or getNumberDetails and map the reason enum to your application’s needs. If the reason field is unused, convert the call to a plain getBooleanValue/getNumberValue and remove the detail call.

temp-discount-banner (stale signal): This flag key triggered a staleness signal because temp is a known staleness indicator. Check LaunchDarkly’s dashboard: if the flag is fully rolled out to 100% of users, remove it from the codebase entirely rather than migrating it. Removing dead flag debt is cheaper than carrying it across the OpenFeature boundary.


The full step-by-step NestJS guide — including module wiring, wrapper detection configuration, and how to handle shared evaluation services — is at /docs/guides/nestjs/.

For the underlying concepts behind how FlagLint classifies flag debt and generates readiness scores, see how FlagLint works.

If your team is evaluating the total migration effort before scheduling work, the flag debt audit article covers how to turn the audit output into an hour estimate you can take to your engineering manager.