Migrating from LaunchDarkly
This guide is for teams that run feature flags on LaunchDarkly and want to move them to ConfigDirector. It maps the LaunchDarkly concepts onto ConfigDirector's, walks through recreating the flags, and shows how to switch each SDK over so that both services run side by side until you remove the LaunchDarkly one.
If you are new to ConfigDirector, read Core Concepts first. It takes a few minutes and makes the mapping below easier to follow.
Before you start
- Pick one LaunchDarkly project to move first. A project with a handful of flags and one or two services is a good rehearsal. Everything below is done per project.
- Make a list of the flags, with their variations and targeting. LaunchDarkly has no flag export, but its REST API lists every flag in a project with its variations and a summary of the targeting in each environment:
GET /api/v2/flags/{projectKey}, with?env=production&summary=falsefor the full rules. See the LaunchDarkly API documentation. For a small project, the flag list in the LaunchDarkly dashboard is enough. - Decide which flags not to move. A migration is the best moment to clean up. A flag whose status is launched or inactive in every environment serves one value and has nothing left to decide. Hardcode that value in the code instead of recreating the flag. Only active flags, and the permanent ones you still rely on, need to move.
- Create your ConfigDirector organization and project. Sign up, create a project, and add an environment for each LaunchDarkly environment you use.
TestandProductionare created for you. Mark the environments that serve real traffic as live.
How the concepts map
Projects, environments, and flags
| LaunchDarkly | ConfigDirector |
|---|---|
| Project | Project |
| Environment | Environment. A critical environment in LaunchDarkly is a live environment in ConfigDirector. |
| Feature flag | Config, with a role: Feature flag for a temporary boolean, Kill switch for a permanent boolean, Experiment for variations you compare, and Config for everything else. |
| Flag type: boolean, string, number, JSON | Config type: Boolean, String, Integer or Float, JSON. ConfigDirector also has Enum, URL, and Timespan, which validate the value as that type. A string flag with a fixed set of variations is usually an Enum. |
| Variations | The values a config serves. Experiments define them as named variations; other roles enter the value in each targeting rule. |
| Temporary and permanent flags | Lifetime: Temporary or Permanent. Temporary configs are tracked through the config lifecycle, which tells you when they have settled and can be removed. |
| Flag key | Config key. LaunchDarkly keys are valid ConfigDirector keys, so keep them: the code change is then only the SDK call. Unlike LaunchDarkly, a key can be renamed safely later, even in production. |
| Client-side SDK availability, mobile key availability | SDK availability: client SDKs, server SDKs, or both. Client and mobile applications both use the client SDK key. |
| Tags | Tags |
| JSON Schema on a flag's variations | JSON Schema validation on a JSON config, on both pricing options. Changing the schema re-validates every value already set in every environment. |
| Flag maintainer, custom properties, flag links | No equivalent. Put the owner in the description, or in a tag. |
| Archived flag | Archived config. It can be restored for 30 days, and then it is purged. |
Targeting
| LaunchDarkly | ConfigDirector |
|---|---|
| Default rule (fallthrough) | The default targeting rule, the last rule in every environment. Its value is served when no other rule matches. |
| Targeting on and off, with the off variation | There is no on/off switch above the rules. To turn a flag off for everyone, remove or reorder its rules so the default targeting rule serves the off value. For a switch that stays independent of the rules, create a Kill switch and check it first in your code. |
| Individual targets | A conditional rule with the condition identifier is one of the listed identifiers. Add a context.kind condition to the rule when the identifiers belong to a context kind other than user. |
| Rule with clauses | A conditional rule with one attribute condition per clause. Every condition of a rule must match, as in LaunchDarkly. There is no OR inside a rule: write a second rule, or use a segment, whose condition groups are combined with OR. |
| Attribute operators: string, number, semver, date, regex | Attribute conditions read a trait as text, a number, a semantic version, a datetime, an array, or a boolean, each with its own operators. There is no regular expression operator. Text conditions are case-sensitive. |
| Percentage rollout, bucketed by a chosen context kind and attribute | A percentage rollout, on its own or as the value of a conditional rule. Contexts are assigned by their context kind and identifier. Choosing another attribute to bucket by is not available: evaluate with the entity you want to bucket by as the context instead, for example kind: "organization". |
| Segment, rule-based, per environment | A segment, defined once per project with one or more condition groups, used by any config in the project. An environment that needs its own definition gets an environment override. |
| Segment, list-based | Not yet available. Use a condition group with identifier is one of the listed identifiers; it is kept in one place and reused the same way. |
| Prerequisites | Not available. Put the condition the prerequisite flag expressed on the dependent flag's own rules, or check both flags in code. |
| Scheduled changes, approvals, guarded rollouts | Not available. Changes take effect as soon as they are saved. |
| Test run | The targeting rules tester, which evaluates saved or unsaved rules against a context and shows why each rule matched or not. |
Contexts
| LaunchDarkly | ConfigDirector |
|---|---|
| Context | Context |
Context kind: user, organization, device, custom | Context kind. Leaving it out means user. Any lowercase name of up to 64 letters, digits, dashes, or underscores works. |
Context key | Identifier, id in the SDKs. A context is identified by its kind and identifier together, so user 42 and device 42 are different contexts. |
Context name | name |
Custom attributes, such as email or plan | Traits, an arbitrary JSON object. Targeting rules address a trait with a JSON pointer, such as /plan or /address/country. |
anonymous: true | An anonymous context: a context given without an identifier, or with anonymous set. It is evaluated but never stored or shown in the dashboard. |
| Multi-context, several kinds in one evaluation | Not available. Each evaluation is for one context. To target a user by their organization, copy the organization's attributes you target by into the user's traits, for example /organization/plan. Or evaluate the config a second time with the organization as the context. |
| Private attributes | Not available. Only put the traits your targeting rules use in the context. |
SDKs and delivery
| LaunchDarkly | ConfigDirector |
|---|---|
| SDK key | Server SDK key, a secret, for server SDKs. |
| Client-side ID and mobile key | Client SDK key, for browser and mobile SDKs. It is not secret, because client SDKs only receive evaluated values. |
| Streaming by default, with polling as an option | The same. See Architecture. |
| Server SDKs evaluate locally | The same. Server SDKs receive the rules and segments and evaluate every context in memory. |
| Client SDKs receive evaluated values | The same. |
| Fallback value in code | The in-code default value, passed with every read. It is returned only when the SDK cannot evaluate the config. |
| Flag change listeners | watch in the JavaScript SDKs, a stream or flow in the mobile SDKs, and the configsUpdated hook. Each SDK page shows them. |
| Bootstrapping from a server-rendered payload | Not available in the browser SDK. The Next.js and Nuxt SDKs evaluate configs during server-side rendering, so the first render carries real values. |
| Relay Proxy | Not needed. Every SDK connects to ConfigDirector directly over streaming. A base url option is available for an HTTP proxy of your own. |
| Test data sources | The test client in each SDK's testing entry point, and the in-memory providers of the OpenFeature SDKs. See Test your code on each SDK page. |
| Experimentation metrics and results | Not available. An Experiment config serves the variations; measure them in your own analytics. Every evaluation carries a valueId, a stable identifier of the value served, that you can send instead of the value itself. |
Recreate the flags
Create the configs in ConfigDirector while the LaunchDarkly flags keep serving your applications. Nothing changes for your users until the SDKs switch over.
For each flag you decided to move:
- Create a config with the same key, the role and type from the tables above, and the lifetime. Set its initial value to what LaunchDarkly serves most users in production today, so an application that reads it before its rules exist gets a safe value.
- Set the SDK availability to match where the flag is read.
- In each environment, recreate the targeting: individual targets as
identifier is one ofrules, rules as conditional rules, the rollout as a percentage rollout, and the fallthrough as the default targeting rule value. - Check a few contexts with the targeting rules tester. The result should be the value LaunchDarkly serves them.
There are three ways to do this:
- The dashboard, for a project with a few flags. The config form and the targeting rules editor cover everything above.
- The Terraform provider, if your LaunchDarkly flags were managed with LaunchDarkly's Terraform provider. The ConfigDirector Terraform provider has
configdirector_configfor the flag andconfigdirector_config_targeting_rulesfor the rules of each environment, with the same structure as the tables above. Translate the resources once and apply. - The MCP server, for a project with many flags that you manage by hand. Connect the MCP server to your coding assistant, give it the flag list from the LaunchDarkly API, and ask it to create the configs and their targeting rules. Review what it created in the dashboard before switching any SDK.
Switch the SDKs
Switch one application at a time. Pass the value LaunchDarkly serves today as the in-code default value, so that a config that is missing, or read before the client is ready, still gives the result users get now.
Server SDKs
A Node.js service reading a flag with the LaunchDarkly server SDK:
import * as LaunchDarkly from "@launchdarkly/node-server-sdk";
const ldClient = LaunchDarkly.init("YOUR-LAUNCHDARKLY-SDK-KEY");
await ldClient.waitForInitialization({ timeout: 10 });
const context = {
kind: "user",
key: "user-123",
name: "Example User",
email: "user@example.com",
plan: "enterprise",
};
const newCheckout = await ldClient.variation("new-checkout", context, false);
The same flag with the ConfigDirector Node.js SDK. The context's custom attributes move under traits, and the key becomes id:
import { createClient } from "@configdirector/server-sdk";
const client = createClient("YOUR-SERVER-SDK-KEY");
await client.initialize();
const context = {
id: "user-123",
name: "Example User",
traits: {
email: "user@example.com",
plan: "enterprise",
},
};
const newCheckout = client.getValue("new-checkout", false, context);
A context of another kind sets kind:
const organization = { id: "acme", kind: "organization", name: "Acme Inc.", traits: { plan: "enterprise" } };
const newCheckout = client.getValue("new-checkout", false, organization);
getValue is synchronous: the server SDK evaluates the rules it already holds in memory. The type of the value is the type of the in-code default value you pass, so a string flag is read with a string default and a JSON flag with an object. The Python, Java, .NET, and Go SDKs follow the same shape.
Browser and mobile SDKs
A web application with the LaunchDarkly JavaScript client SDK:
import * as LDClient from "launchdarkly-js-client-sdk";
const ldClient = LDClient.initialize("YOUR-CLIENT-SIDE-ID", {
kind: "user",
key: "user-123",
name: "Example User",
plan: "enterprise",
});
await ldClient.waitForInitialization(5);
const newCheckout = ldClient.variation("new-checkout", false);
ldClient.on("change:new-checkout", (value) => render(value));
await ldClient.identify({ kind: "user", key: "user-456" });
The same with the ConfigDirector JavaScript SDK. The context is given to initialize, changes arrive through watch, and identify becomes updateContext:
import { createClient } from "@configdirector/client-sdk";
const client = createClient("YOUR-CLIENT-SDK-KEY");
await client.initialize({
id: "user-123",
name: "Example User",
traits: { plan: "enterprise" },
});
const newCheckout = client.getValue("new-checkout", false);
client.watch("new-checkout", false, (value) => render(value));
await client.updateContext({ id: "user-456" });
initialize resolves when the first values arrive or after its timeout, 3 seconds by default; after a timeout the client keeps connecting in the background and reads return the in-code default value until values arrive. The React, Vue, React Native, Flutter, Swift, and Android SDKs follow the same shape, with a hook, composable, stream, or flow in place of watch.
Applications on OpenFeature
If your code reads flags through OpenFeature with the LaunchDarkly provider, the code does not change: register the ConfigDirector provider instead. The targetingKey becomes the context's identifier. Attributes the ConfigDirector provider reads are kind, name, traits, and anonymous, so move the custom attributes your rules use into a traits attribute. See OpenFeature with ConfigDirector for the providers and the full mapping.
Which SDK replaces which
| LaunchDarkly SDK | ConfigDirector SDK |
|---|---|
| JavaScript client | JavaScript |
| React Web | React |
| Vue | Vue |
| React Native | React Native |
| Flutter | Flutter |
| iOS | Swift |
| Android | Android |
| Node.js server, Python, Java, .NET server, Go | Node.js, Python, Java, .NET, Go |
| Next.js integration | Next.js. ConfigDirector also has a Nuxt SDK. |
| OpenFeature providers | OpenFeature providers for the web, Swift, Android, Flutter, Node.js, Python, Java, .NET, and Go |
ConfigDirector has no SDK for Ruby, PHP, Rust, C++, Erlang, Haskell, Lua, Roku, or Electron, and no edge SDKs. If you need one of them, write to sdks@configdirector.com with the SDK you are looking for and we will let you know the projected date for its release.
Verify, then remove LaunchDarkly
Once an application reads a config from ConfigDirector:
- Evaluation Activity on the config shows the values being served in each environment. See Evaluation Activity.
- Alerts catch the mistakes a migration makes: a key the application asks for that does not exist, a config read with the wrong type, an in-code default value that drifts from the value served, and the default value served more often than expected. See Alerts.
- For a flag you rolled out by percentage, the same identifier gets the same value on every evaluation, as in LaunchDarkly. The split itself is assigned afresh, so a user who was in LaunchDarkly's 10% is not necessarily in ConfigDirector's. Switch the SDK over at a point in the rollout where that is acceptable, or at 0% or 100%.
When every application has switched, remove the LaunchDarkly SDK and its keys from the code, and archive the flags in LaunchDarkly. From then on, the config lifecycle tells you when each temporary config has settled and can be removed from the code too.
Metering
The two services meter different things, which matters for the bill once you are past the free allowance. LaunchDarkly counts client-side monthly active users and service connections. ConfigDirector counts client sessions, which are client SDK instances with a session that lasts up to 6 hours in a browser, service connection hours of your server SDKs, and Admin API requests. Pricing and billing explains the three and the free allowance, which applies on both pricing options.
Admin API
Manage projects, environments, configs, targeting rules, segments, and tags over HTTPS with an API token. The base URL, authentication, permissions, error responses, usage counting, the endpoints, and the OpenAPI document.
From Firebase Remote Config
Move your remote configuration from Firebase Remote Config to ConfigDirector. How parameters, conditions, in-app defaults, and fetch and activate map, how to recreate the parameters, and how to switch the Flutter, Android, iOS, and web SDKs over.