Migrating from LaunchDarkly
Step-by-step guide to moving feature flags, evaluation context, and SDK calls from LaunchDarkly to Flipswitch via OpenFeature.
This guide walks through migrating an existing LaunchDarkly setup to Flipswitch. The shape of the migration depends on how you're using LaunchDarkly today:
- Already on the OpenFeature SDK with the LaunchDarkly provider: swap one provider initialization line. Every
client.getBooleanValue(...)in your codebase stays untouched. - On the raw
LDClientSDK: the call sites change shape (fromclient.variation('flag-key', user, false)toawait client.getBooleanValue('flag-key', false, context)), but the rewrite is mechanical and search-and-replaceable.
Either way, the migration is reversible. You run both providers in parallel, verify parity, then cut over with a feature flag.
Most things port cleanly. A few LD-specific features (experiments, Big Segments, scheduled changes) need separate handling. See the "What doesn't migrate cleanly" section before you start, so nothing is a surprise mid-cutover.
Before you start
You'll need:
- A Flipswitch account. The free tier is enough to dry-run the migration.
- LaunchDarkly admin or owner access. You'll be reading flag definitions, segments, and rules, either from the dashboard or via LD's REST API.
- An inventory of what you use:
- Which SDKs and which languages.
- Total flag count, segment count, and the rough complexity of your targeting rules.
- Anything LD-specific: experiments with metrics, Big Segments, scheduled flag changes, multi-context evaluation.
Knowing the inventory upfront tells you whether the dashboard is enough or whether you'll want a coding agent (via the Flipswitch MCP server) to bulk-create flags from your LD export.
Step 1: Set up the Flipswitch SDK alongside LaunchDarkly
Install the Flipswitch provider for your language without removing LaunchDarkly. You'll initialize both clients and run them in parallel through Step 5.
// Add Flipswitch alongside the existing LD SDK
import { FlipswitchServerProvider } from '@flipswitch-io/server-provider';
import { OpenFeature } from '@openfeature/server-sdk';
import * as LaunchDarkly from 'launchdarkly-node-server-sdk';
// Existing LD client (keep this for now)
const ldClient = LaunchDarkly.init('LD_SDK_KEY');
await ldClient.waitForInitialization();
// New Flipswitch provider
const flipswitchProvider = new FlipswitchServerProvider({
apiKey: process.env.FLIPSWITCH_API_KEY!,
});
await OpenFeature.setProviderAndWait(flipswitchProvider);
const ofClient = OpenFeature.getClient();# Add Flipswitch alongside the existing LD SDK
import ldclient
from ldclient.config import Config
from openfeature import api
from flipswitch import FlipswitchProvider
# Existing LD client (keep this for now)
ldclient.set_config(Config('LD_SDK_KEY'))
ld = ldclient.get()
# New Flipswitch provider
api.set_provider(FlipswitchProvider(api_key=os.environ['FLIPSWITCH_API_KEY']))
of_client = api.get_client()// Add Flipswitch alongside the existing LD SDK
import (
"github.com/launchdarkly/go-server-sdk/v7"
"github.com/open-feature/go-sdk/openfeature"
flipswitch "github.com/flipswitch-io/go-provider"
)
// Existing LD client (keep this for now)
ldClient, _ := ld.MakeClient("LD_SDK_KEY", 5*time.Second)
// New Flipswitch provider
openfeature.SetProvider(flipswitch.NewProvider(os.Getenv("FLIPSWITCH_API_KEY")))
ofClient := openfeature.NewClient("my-app")// Add Flipswitch alongside the existing LD SDK
import com.launchdarkly.sdk.server.LDClient;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Client;
import io.flipswitch.FlipswitchProvider;
// Existing LD client (keep this for now)
LDClient ldClient = new LDClient("LD_SDK_KEY");
// New Flipswitch provider
FlipswitchProvider provider = FlipswitchProvider
.builder(System.getenv("FLIPSWITCH_API_KEY"))
.build();
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
Client ofClient = OpenFeatureAPI.getInstance().getClient();For full SDK setup (configuration options, real-time updates, framework integrations), see the language-specific reference: JavaScript, Python, Go, Java.
Step 2: Map evaluation context
LaunchDarkly's user/context shape doesn't match OpenFeature's exactly. Write a small mapper and use it everywhere you currently build an LDUser or LDContext.
The two shapes:
| LaunchDarkly | OpenFeature |
|---|---|
{ key: 'user-123', email: '...', custom: { plan: 'pro', region: 'eu' } } | { targetingKey: 'user-123', email: '...', plan: 'pro', region: 'eu' } |
Mapping rules:
- LD
keybecomes OpenFeaturetargetingKey. - LD
email,name,firstName,lastNamestay as top-level attributes. - Everything inside
custombecomes a flat top-level attribute. - LD's newer multi-context shape (
{ kind: 'user', key, ... }) is already flat. Drop thekindfield; renamekeytotargetingKey.
import type { LDUser } from 'launchdarkly-node-server-sdk';
import type { EvaluationContext } from '@openfeature/server-sdk';
export function ldUserToOpenFeature(user: LDUser): EvaluationContext {
const { key, custom, ...rest } = user;
return {
targetingKey: key,
...rest,
...(custom ?? {}),
};
}Percentage-rollout buckets do not survive the migration. The same targetingKey produces a different bucket in Flipswitch than it did in LaunchDarkly, because the two systems use different hash functions and salts. A user who was in "the 25% who see X" in LD will land somewhere different in Flipswitch.
Plan for this. Either hold percentage rollouts at 0% or 100% during cutover (so bucket assignment doesn't matter), or accept that the membership of partial-rollout flags will shuffle once at switchover. Targeting-rule matches (segment membership, attribute equality) are unaffected.
Step 3: Recreate flags in Flipswitch
Three approaches, depending on flag count and tooling.
Dashboard. Recreate each flag manually in the Flipswitch dashboard. Fine for under ~20 flags, and honestly the path most teams take. The field-mapping table below tells you which LaunchDarkly field goes where.
MCP-driven bulk creation. If you have more flags, or just prefer not to click through a UI, the Flipswitch MCP server exposes a create_flag tool to any MCP-capable coding agent (Claude Code, Cursor, and similar). Hand the agent your LaunchDarkly export plus the field-mapping table below, and it can loop through and create each flag for you. The agents setup guide covers how to connect.
White-glove migrations. For larger or more involved setups (hundreds of flags, complex segment graphs, regulated environments), email hello@flipswitch.io with your LD export and we'll help directly. A first-class public Admin API is on the roadmap but isn't shipped yet, so for now this is the path for anything beyond what the MCP route handles cleanly.
The field mapping below applies to all three approaches:
| LaunchDarkly | Flipswitch |
|---|---|
Flag kind: 'boolean' | Boolean flag |
Flag kind: 'multivariate' (string / number / json) | String / number / JSON flag with named variants |
variations array | Variants (one entry per LD variation; preserve indices/keys) |
defaults.onVariation / defaults.offVariation | Default variant when targeting "off" |
targets (per-variation user lists) | Targeting rules with targetingKey IN [...] |
rules (clause-based targeting) | Targeting rules (operator names mostly map directly) |
segments | Segments (rebuild segments separately, then reference) |
tags | Flag description (Flipswitch doesn't have tags as a first-class field today) |
maintainer | Note in description (ownership lives at the project level, not per flag) |
Recreate segments before flags that reference them. Flag rules that point at a non-existent segment will fail to import.
Step 4: Replace SDK call sites
Once the flags exist in Flipswitch, point your call sites at the OpenFeature client. The shape change is straightforward:
// Before (LaunchDarkly)
const showFeature = await ldClient.variation(
'checkout-redesign',
{ key: user.id, email: user.email, custom: { plan: user.plan } },
false,
);
// After (OpenFeature + Flipswitch)
const context = {
targetingKey: user.id,
email: user.email,
plan: user.plan,
};
const showFeature = await ofClient.getBooleanValue(
'checkout-redesign',
false,
context,
);# Before (LaunchDarkly)
user = {'key': user_id, 'email': email, 'custom': {'plan': plan}}
show_feature = ld.variation('checkout-redesign', user, False)
# After (OpenFeature + Flipswitch)
context = EvaluationContext(targeting_key=user_id, attributes={
'email': email,
'plan': plan,
})
show_feature = of_client.get_boolean_value('checkout-redesign', False, context)// Before (LaunchDarkly)
ldUser := lduser.NewUserBuilder(userID).Email(email).Custom("plan", ldvalue.String(plan)).Build()
showFeature, _ := ldClient.BoolVariation("checkout-redesign", ldUser, false)
// After (OpenFeature + Flipswitch)
ctx := openfeature.NewEvaluationContext(userID, map[string]interface{}{
"email": email,
"plan": plan,
})
showFeature, _ := ofClient.BooleanValue(context.Background(), "checkout-redesign", false, ctx)// Before (LaunchDarkly)
LDContext ldContext = LDContext.builder(user.getId())
.set("email", user.getEmail())
.set("plan", user.getPlan())
.build();
boolean showFeature = ldClient.boolVariation("checkout-redesign", ldContext, false);
// After (OpenFeature + Flipswitch)
MutableContext ctx = new MutableContext(user.getId());
ctx.add("email", user.getEmail());
ctx.add("plan", user.getPlan());
boolean showFeature = ofClient.getBooleanValue("checkout-redesign", false, ctx);The same pattern extends to string, number, and JSON flags: getStringValue, getNumberValue, getObjectValue. See the JavaScript SDK reference for the full set of typed methods.
LD's server SDKs evaluate locally (the SDK has the rules), so variation() is sync in many languages. OpenFeature's server-side getBooleanValue is async because Flipswitch evaluates server-side. The web SDK is sync after init in both vendors. If you have call sites in tight loops, evaluate once at the boundary (request handler, component mount) and pass the result down, the same pattern that's good practice with any feature flag SDK.
Step 5: Run side-by-side and verify parity
Don't cut over yet. Wrap the two clients in a small comparator that calls both and logs disagreements. After running this through a real traffic cycle, the log tells you which flag rules you missed when recreating.
async function compare(flagKey: string, defaultValue: boolean, ldUser: LDUser) {
const ofContext = ldUserToOpenFeature(ldUser);
const [ldResult, ofResult] = await Promise.all([
ldClient.variation(flagKey, ldUser, defaultValue),
ofClient.getBooleanValue(flagKey, defaultValue, ofContext),
]);
if (ldResult !== ofResult) {
logger.warn('Flag divergence', { flagKey, ldResult, ofResult, userId: ldUser.key });
}
return ldResult; // still serving from LD during verification
}What you'll see in the divergence log:
- Percentage rollouts diverging on individual users: expected, see the callout in Step 2. The aggregate rate should be close to the configured percentage.
- Targeting-rule mismatches: unexpected. Each one is a rule you missed when recreating the flag in Flipswitch. Fix the flag, the comparator stops complaining.
- Default-value mismatches: the flag exists in LD but not in Flipswitch, or has a different "off" variant. Worth catching before users see it.
Run the comparator for at least one full traffic cycle, longer if your targeting paths only fire on certain days or releases.
Step 6: Cut over
Once divergences are zero (excluding intentional percentage shifts), cut over with a Flipswitch flag.
Create a flag called use-flipswitch-evaluator in Flipswitch, default false. Update your code to check it before deciding which client to use:
const useFlipswitch = await ofClient.getBooleanValue('use-flipswitch-evaluator', false);
const showFeature = useFlipswitch
? await ofClient.getBooleanValue('checkout-redesign', false, ofContext)
: await ldClient.variation('checkout-redesign', ldUser, false);Roll use-flipswitch-evaluator from 0% to 100% the same way you'd roll out any other change: small percentages first, watch error rates and a couple of business KPIs, expand. Because this is itself a Flipswitch flag, the rollback is instant: flip the flag back to false and SSE pushes the change to all SDKs in milliseconds.
When you're confident, remove the comparator, the LD SDK, and the dual-client logic. Cancel the LD subscription.
What doesn't migrate cleanly
Be honest about these before you start.
Experiments and metrics tracking. LaunchDarkly's experimentation product (assignments, metrics, statistical significance) doesn't have a direct counterpart in Flipswitch today. If you're running active experiments, the practical options are:
- Keep LD for experiment-flagged code paths during the migration. Cut over the rest first.
- Move metrics and experimentation to a dedicated tool (Statsig, GrowthBook, your own warehouse + analysis) and use Flipswitch for the assignment/rollout half.
- Wait. We're working on first-class experiment support; if your timeline is flexible, talk to us.
Big Segments. LD's Big Segments use a separate store for segments above ~100k members. In Flipswitch, segments scale through the same evaluation path, but check your plan's segment-size limit before assuming a 1M-row segment will Just Work.
Scheduled flag changes. LD lets you schedule a flag change for a future timestamp. Flipswitch doesn't ship that today. The current options are flipping the flag manually at the scheduled time, or having a coding agent do it on a cron via the MCP server. We're tracking first-class scheduling for a future release.
Audit log history. Flag-change history doesn't carry over from LD. Audit logs in Flipswitch start at the moment the flag is created. If you need historical audit data for compliance, export your LD audit log before cancelling.
Mobile SDKs. Flipswitch supports the languages listed under SDKs. If you depend on iOS Swift or Android Kotlin specifically, check whether your platforms are covered there before committing to a date.
Stuck on something specific? Email hello@flipswitch.io with what you're trying to migrate and we'll write back with a concrete answer, often within a day.
Next steps
- Set up the SDK for your language: JavaScript, Python, Go, Java.
- Get familiar with Targeting Rules and Segments so the field-mapping table in Step 3 makes sense.
- Connect a coding agent via the Flipswitch MCP server if you want to script bulk flag creation through your editor.