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.
Four things to understand before starting:
- OpenFeature is a vendor-neutral evaluation API standard — it does not replace LaunchDarkly. LaunchDarkly may continue to evaluate your flags through its official OpenFeature provider.
- The flag service does not change. Your existing LaunchDarkly flags, targeting rules, and environments stay exactly as they are.
- The migration moves the evaluation boundary into application code. You are changing how your application calls the flag service, not what the flag service does.
- FlagLint does not configure or operate the provider. FlagLint analyzes your source code and rewrites call sites. Initializing the OpenFeature client with a LaunchDarkly provider is a separate manual step covered in the OpenFeature Provider Setup guide.
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.
// 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:
- return ldClient.boolVariation("checkout-v2", context, false); + return flags.getBooleanValue("checkout-v2", false, context);
A mechanical migration can still produce incorrect code, especially in:
- JavaScript — no type enforcement at all
- Loosely typed or
any-typed TypeScript — the compiler cannot catch the mismatch - Wrapper APIs or codemods — tools that transpose the method name without also transposing the arguments
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.
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 (stringVariation → getStringValue) |
| Static number evaluation | Usually safe | Direct mapping exists (numberVariation → getNumberValue) |
| 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. |
Complete migration workflow
FlagLint provides four commands that form a complete migration workflow. Run them in order.
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.
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.
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-dirtyto override
Exit code: 0 on success; non-zero if preconditions fail.
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.
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.
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:
✓ 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.
// 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.
// 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.
return ldClient.boolVariation(flagKey, ldContext, defaultValue);
FlagLint reports:
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.
return ldClient.boolVariationDetail(flagKey, context, false);
FlagLint reports:
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.
- Source-code migrationFlagLint handles: rewrites call sites, transposes arguments, renames methods
- OpenFeature client initializationManual setup: initialize OpenFeature with your chosen provider before evaluations
- LaunchDarkly provider configurationManual setup: register the LaunchDarkly OpenFeature provider with your SDK key
- Application rollout and testingManual: verify flag behavior is unchanged in staging before promoting to production
- CI enforcementFlagLint handles:
flaglint validate --no-direct-launchdarklygates the boundary in CI
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.
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:
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:
✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found. Scanned 5 file(s).
GitHub Actions snippet:
- name: Enforce OpenFeature boundary run: npx flaglint@latest validate ./src --no-direct-launchdarkly
Get started
Run the audit command to see where you stand. FlagLint runs entirely locally — your source code does not leave your machine.
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.)