Migrating LaunchDarkly Node.js SDK Usage to OpenFeature: A Safe, Practical Guide

What changes when adopting OpenFeature

Adopting OpenFeature changes where in your application the flag evaluation boundary lives. Today, most LaunchDarkly Node.js codebases call the LaunchDarkly SDK directly from business logic — every boolVariation, stringVariation, or jsonVariation call is a direct dependency on the LaunchDarkly SDK.

Before
Application code
LaunchDarkly SDK
LaunchDarkly service

After
Application code
OpenFeature API
LaunchDarkly provider
LaunchDarkly service

Four things to understand before starting:

Why the migration is deceptively risky

If you were to migrate by hand or with a naive find-and-replace, you would almost certainly introduce a silent runtime bug. The root cause is an argument-order inversion between the LaunchDarkly SDK and the OpenFeature API.

Argument order comparison
// LaunchDarkly: (key, context, fallback)
ldClient.boolVariation("checkout-v2", context, false);

// OpenFeature: (key, fallback, context)
openFeatureClient.getBooleanValue("checkout-v2", false, context);

The equivalent migration diff looks like this:

checkout.ts
- return ldClient.boolVariation("checkout-v2", context, false);
+ return flags.getBooleanValue("checkout-v2", false, context);

A mechanical migration can still produce incorrect code, especially in:

In strictly typed, direct SDK usage, TypeScript may catch the incorrect ordering — but teams should not rely on type checking as their migration strategy. Type errors depend on how the client and context types are declared; they are not guaranteed.

This is a correctness hazard, not necessarily a compile-time error. Your application may start and your tests may pass — but flags can evaluate against wrong context data until someone notices unexpected behavior in production. FlagLint rewrites both the method name and the argument order together, atomically, only when it can prove the transformation is safe.

Migration-pattern classification

Not all LaunchDarkly call sites are equally safe to rewrite automatically. FlagLint classifies every detected call site by migration pattern before deciding whether to rewrite or report for manual review.

Pattern Automation status Reason
Static boolean evaluation Usually safe Flag key is a literal; argument types and positions are known
Static string evaluation Usually safe Direct mapping exists (stringVariationgetStringValue)
Static number evaluation Usually safe Direct mapping exists (numberVariationgetNumberValue)
Static JSON/object evaluation Usually safe with parity review Type mapping exists; review expected value shape
Dynamic flag key Manual review Flag key is a variable — FlagLint cannot verify the target flag at compile time
Detail evaluation Manual review variationDetail/boolVariationDetail return metadata; OpenFeature detail APIs differ
Bulk flag evaluation Manual review allFlagsState has no single-flag codemod; requires architecture decision
Internal wrapper function Configuration or review required Internal wrappers require manual review or explicit configuration. FlagLint does not assume that arbitrary wrapper methods are semantically equivalent to direct SDK evaluations.
FlagLint rewrites only cases it can prove are safe. Any call site where the flag key, argument types, fallback value, or OpenFeature client binding cannot be statically proven is left unchanged and reported for manual review.

Complete migration workflow

FlagLint provides four commands that form a complete migration workflow. Run them in order.

1
Audit — understand your exposure
bash
npx flaglint@latest audit ./src

When to run: Before any migration work begins, and periodically as the codebase evolves.

What it does: Scans source files for direct LaunchDarkly SDK calls, classifies each usage by migration risk, and produces a readiness score. Add --format json or --format html for machine-readable or browser-viewable output. Add --cost-estimate for a directional effort estimate.

  • Does not modify any files
  • Does not require API credentials or network access
  • Does not upload your source code anywhere

Exit code: Always 0 — this command is informational only.

2
Preview — review the diffs before applying
bash
npx flaglint@latest migrate ./src --dry-run

When to run: After reviewing the audit output, before applying any changes.

What it does: Shows exactly which call sites will be rewritten and what the rewritten code looks like — as a reviewable diff — without touching any files. Output includes the count of automatable vs. skipped calls and the reason each skipped call was left unchanged.

  • Does not modify any files

Exit code: Always 0 — this command is informational only.

3
Apply — write the proven-safe rewrites
bash
npx flaglint@latest migrate ./src --apply

When to run: After reviewing the dry-run output on a clean git branch.

What it does: Applies proven-safe rewrites to source files in place. Only call sites that pass FlagLint's safety analysis are rewritten; manual-review call sites are not touched.

  • Does not touch flagged manual-review call sites
  • Does not rewrite without a proven OpenFeature binding
  • Requires a clean git working tree by default; pass --allow-dirty to override

Exit code: 0 on success; non-zero if preconditions fail.

4
Validate — enforce the boundary in CI
bash
npx flaglint@latest validate ./src --no-direct-launchdarkly

When to run: In CI after migration is complete; also locally to confirm the boundary is clean. See the validate CLI reference for all options.

What it does: Fails with a non-zero exit code if any direct LaunchDarkly evaluation calls remain in the scanned files.

  • Does not pass if unapproved direct calls remain

Exit code: 0 if no direct LaunchDarkly calls are found; 1 if any are found.

Enable strict enforcement only after all manual-review call sites have been resolved — either rewritten, wrapped, or explicitly reviewed and documented. If any intentional LaunchDarkly calls remain (for example, in a wrapper still under active review), strict validation will fail until they are addressed.

Enterprise checkout service case study

This case study uses the FlagLint enterprise-checkout-service demo fixture, a realistic checkout service with intentionally mixed patterns — static evaluations, dynamic keys, detail calls, and a shared wrapper function.

enterprise-checkout-service Verified · FlagLint v0.7.0
Source files scanned 5
Direct LaunchDarkly calls 20
Unique flags 13
High risk 3
Medium risk 10
Safely automatable 10
Manual review required 10
Reviewable diffs 10
Skipped calls 10
Files changed by migrate 3

The three changed files are checkout.ts, pricing.ts, and product.ts — the files that contained only static-key evaluations. The remaining two files (analytics.ts and flags-wrapper.ts) were left unchanged because they contain dynamic keys, detail calls, or bulk evaluation patterns that require manual review.

Actual audit output from flaglint audit:

Terminal output
✓ Audit complete: 13 flags — 3 high risk, 10 medium risk

Migration readiness: 50/100  ·  moderate
[█████████████░░░░░░░░░░░░] 50%
10 safely automatable  ·  10 require manual review

A readiness score of 50/100 means roughly half the call sites in this codebase require manual review before migration can be declared complete. In this fixture, that is primarily due to dynamic flag keys inside a shared wrapper function in flags-wrapper.ts that accepts a flag key as a parameter. Resolving those 10 manual-review calls — for example, by extracting keys to constants where possible — would push the score toward 100/100.

Rewrite and refusal examples

The following examples come directly from flaglint migrate --dry-run output on the enterprise-checkout-service fixture.

Example 1 — Boolean evaluation Rewritten
checkout.ts
// Before
return ldClient.boolVariation("checkout-v2", ctx, false);
// After
return openFeatureClient.getBooleanValue("checkout-v2", false, ctx);

FlagLint confirms that "checkout-v2" is a string literal, that ctx is an evaluation context, and that false is a boolean fallback. The argument order is transposed and the method is renamed atomically.

Example 2 — String evaluation Rewritten
pricing.ts
// Before
return ldClient.stringVariation("payment-provider", ctx, "stripe");
// After
return openFeatureClient.getStringValue("payment-provider", "stripe", ctx);

The same pattern applies to string evaluations. stringVariation maps to getStringValue; argument positions are transposed; the string literal key is preserved exactly.

Example 3 — Dynamic flag key Left unchanged
flags-wrapper.ts — not rewritten
return ldClient.boolVariation(flagKey, ldContext, defaultValue);

FlagLint reports:

flaglint output
Manual review required: dynamic flag key

flagKey is a function parameter — its value is not known at compile time, so FlagLint cannot verify which OpenFeature method to call or confirm the fallback type. This is a safety feature: rewriting dynamic keys could silently break flag routing if the key string resolves differently at runtime. The correct resolution is to extract the key to a constant where possible, or manually review and rewrite each call site after confirming the key's range of values.

Example 4 — Detail evaluation Left unchanged
analytics.ts — not rewritten
return ldClient.boolVariationDetail(flagKey, context, false);

FlagLint reports:

flaglint output
Manual review required: detail methods skipped — OpenFeature detail APIs exist,
but LaunchDarkly/OpenFeature detail result parity requires manual review

boolVariationDetail returns an LDEvaluationDetail object with reason and variationIndex fields alongside the boolean value. OpenFeature has equivalent detail APIs, but the result shape and field semantics differ between the two SDKs. FlagLint does not rewrite detail calls because doing so could produce code that compiles correctly but reads the wrong fields at runtime. Manual review ensures field-by-field parity before you proceed.

Provider boundary

Rewriting application call sites is necessary but not sufficient. Before the rewritten code will work in production, the OpenFeature client must be initialized with a LaunchDarkly provider that routes evaluation calls to the LaunchDarkly service. That is a separate manual setup step — see the OpenFeature Provider Setup guide for instructions.

Preventing regression in CI

Once the migration is complete, add a validation step to CI to ensure no new direct LaunchDarkly calls are introduced as the codebase evolves.

bash
npx flaglint@latest validate ./src --no-direct-launchdarkly

After the 10 safe rewrites only — 10 manual-review calls still remain in analytics.ts and flags-wrapper.ts. Strict validation fails at this intermediate state:

Intermediate state — exit code: 1
10 direct LaunchDarkly calls remain. Resolve manual-review findings before enforcing this gate.

Once all 10 manual-review calls have been separately resolved or removed, validation passes. The output below is from the enterprise-checkout-service after-complete fixture — a completed-state demonstration where those calls have already been resolved:

Completed-state demonstration — exit code: 0
✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
  Scanned 5 file(s).

GitHub Actions snippet:

.github/workflows/ci.yml
- name: Enforce OpenFeature boundary
  run: npx flaglint@latest validate ./src --no-direct-launchdarkly
Enable strict enforcement only after all direct LaunchDarkly calls are resolved. If any manual-review call sites remain — dynamic keys, detail calls, bulk evaluation, or wrapper functions still under review — validation will fail until they are addressed. Resolve, wrap, or explicitly document approved exceptions before enabling this CI gate.

Get started

Run the audit command to see where you stand. FlagLint runs entirely locally — your source code does not leave your machine.

Start here — run this in your project root
npx flaglint@latest audit ./src

FlagLint runs locally. Your source code does not leave your machine.

10 Repository Audit Program

Using LaunchDarkly in a Node.js codebase? Join the 10 Repository Audit Program. Run FlagLint locally and share only anonymized totals — flag counts, risk breakdown, and readiness score. No source code, no file paths, no flag keys leave your machine.

Express interest →

(This is an interest-collection link. No data is submitted automatically.)

View on GitHub | npm package | Documentation | Migration readiness explained | Safety model