Migrating from Firebase Remote Config
This guide is for apps that read their remote configuration from Firebase Remote Config and want to move it to ConfigDirector. It maps the Remote Config concepts onto ConfigDirector's, walks through recreating the parameters as configs, and shows how to switch each SDK over so that both services run side by side until you remove Remote Config.
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
- Export the Remote Config template. The Firebase console, the Firebase CLI (
firebase remoteconfig:get -o remoteconfig.template.json), and the REST API all export the template as JSON, with every parameter, its default and conditional values, its value type, and the project's conditions. That file is the list you work from. See Firebase's template documentation. - Decide which parameters not to move. A migration is the best moment to clean up. A parameter that has had the same value everywhere since it was added, or that no shipped version of the app still reads, has nothing left to decide. Hardcode that value instead of recreating the parameter.
- Create your ConfigDirector organization and project. Sign up and create one project for the app. If you kept one Firebase project per stage, such as
my-app-devandmy-app-prod, they become environments of this one project:TestandProductionare created for you, and you can add more. Mark the environments that serve real traffic as live.
How the concepts map
Parameters and values
| Firebase Remote Config | ConfigDirector |
|---|---|
| Firebase project, one per stage | One project with an environment per stage. Each environment has its own SDK keys and its own targeting rules for every config. |
| Parameter | Config. A boolean parameter that gates a feature is a config with the Feature flag role if it is temporary, or Kill switch if it stays; everything else is a Config. |
| Value type: string, boolean, number, JSON | Config type: String, Boolean, Integer or Float, JSON. ConfigDirector also has Enum, URL, and Timespan, which validate the value as that type. A string parameter with a fixed set of values is an Enum; a string holding a URL is a URL; a number of milliseconds or seconds is a Timespan. |
| Default value | The default targeting rule value: the value of the last rule in each environment, served when no other rule matches. |
| Conditional value | A conditional rule above the default targeting rule, with the condition and the value it serves. Rules are checked in order; the first that matches serves its value. |
| Condition, named and shared across parameters | Conditions are written on each rule. A condition you reuse across many configs becomes a segment, defined once per project and used by a rule as "the context is in the segment". |
| Parameter group | Tags |
| Description | Description |
| Parameter key | Config key. Firebase keys made of letters, digits, and underscores are valid ConfigDirector keys, so keep them: the code change is then only the SDK call. A key can be renamed safely later, even in production. |
In-app default values, from XML, plist, JSON, or defaultConfig | The in-code default value, passed with every read. It is returned when the client has not received values yet, and when the config is missing or has the wrong type. |
| Template versions, diff, and rollback | The config's audit log, with who changed what, from where, and the before and after values. There is no one-click rollback: restore a value by setting it again. |
| Template JSON, exported and published through the CLI or REST | The Terraform provider, the MCP server, and the Admin API. Configs and targeting rules are managed as code with the same validations as the dashboard. |
| Value limits: 3,000 parameters, 1,000,000 characters | 100 configs on Free, unlimited on Pay as you go. 16 KB per value and 1 MB per project. See Pricing and billing. |
Conditions
Remote Config evaluates values per app instance, from signals it collects itself. ConfigDirector evaluates values per context: the user, device, or other entity your app says it is evaluating for, with the traits your app gives it. Most Remote Config conditions become a condition on a trait that your app sets.
| Remote Config condition | ConfigDirector |
|---|---|
| App version, build number | An appVersion condition, compared as a semantic version. The mobile SDKs read the app version from the platform when you do not set it; the browser SDK takes it from the metadata option. |
| App, platform | An appName condition. Give each platform build its own name through the metadata option, such as my-app-ios and my-app-android, or use one config per platform. |
| Country or region, language, device category, OS | A condition on a trait your app sets, such as /country, /language, or /platform. ConfigDirector does not collect these on its own; put the ones your rules use into the context's traits. |
| User property, Analytics audience, custom signal | A condition on a trait, or a segment with the audience's definition. Traits can hold strings, numbers, booleans, arrays, and nested objects, and a condition reads a trait as text, a number, a semantic version, a datetime, an array, or a boolean. |
| User in random percentile | A percentage rollout. Each context is assigned by its context kind and identifier, so a user keeps the same value on every device they sign in on, and across app restarts. |
| First open time | A trait your app sets, such as /firstOpenedAt, compared as a datetime. |
| Date and time | Not available. There are no scheduled changes; change the rule when the time comes, or apply the change from a scheduled job through the Admin API, the Terraform provider, or the MCP server. |
| Imported segment | A segment with the same definition, or a condition group with identifier is one of the listed identifiers. |
Remote Config combines the clauses of a condition with AND, and so does a ConfigDirector rule: every condition of a rule must match. Where Remote Config applies the first matching conditional value in the order the conditions are listed, ConfigDirector applies the first matching rule.
Experiments and rollouts
| Firebase | ConfigDirector |
|---|---|
| A/B Testing experiment on a parameter | A config with the Experiment role. It defines named variations and serves them to segments or by percentage. ConfigDirector does not measure the outcome: send the variation to your analytics, using the valueId every evaluation carries if you prefer not to send the value itself. |
| Rollout with Crashlytics and Analytics metrics | A percentage rollout that you widen as you watch your own monitoring. Alerts tell you about problems with the config itself, such as a type mismatch or the in-code default value being served too often. |
Fetching values
| Firebase Remote Config | ConfigDirector |
|---|---|
| Fetch, then activate | initialize once, when the app starts. There is no activate step: values are ready to read as soon as initialize completes. |
| Minimum fetch interval, 12 hours by default | None. The SDK keeps a streaming connection open and receives every change within about a second of it being saved. Polling is available for environments where a persistent connection does not work. See Architecture. |
| Real-time config update listener, then activate | watch a config, or subscribe to the stream or flow for its key. It emits the current value and then every change. Nothing has to be activated. |
| Values persist on the device across restarts | Client SDKs hold values in memory. After a start, reads return the in-code default value until the first values arrive, which is usually well under a second. initialize waits for them for up to its timeout, 3 seconds by default, and the client keeps connecting in the background after a timeout. |
| Value source: static, default, remote | The evaluation reason: found-match when a value from ConfigDirector was served, or a reason such as client-not-ready, config-state-missing, or type-mismatch when the in-code default value was. Every SDK exposes it, through a hook or an evaluation method. |
| Firebase installation as the evaluated entity | The context: give it the user's identifier when they sign in, or leave the identifier out for an anonymous context, for which ConfigDirector derives a stable identifier from the SDK instance. A context of another kind, such as a device, sets kind. |
| Server-side Remote Config, in the Admin SDK | The server SDKs, with a server SDK key. They receive the targeting rules and evaluate every context locally. |
| Pricing: 100,000 fetches a day free, then per fetch | A free allowance of 40,000 client sessions a month, then per session. A session is one client SDK instance for as long as it is used, however many values it reads or updates it receives. See Pricing and billing. |
Recreate the parameters
Create the configs in ConfigDirector while Remote Config keeps serving your app. Nothing changes for your users until the SDKs switch over.
For each parameter in the exported template:
- Create a config with the same key, the role and type from the tables above, and a lifetime:
Temporaryfor a parameter you plan to remove once a feature is fully released,Permanentfor one that stays. Set its initial value to the parameter's default value. - In each environment, recreate the conditional values as conditional rules, in the same order, with the parameter's default value as the default targeting rule value. A Firebase project per stage maps onto one environment each; a single Firebase project maps onto
Production, andTestgets the values you want while developing. - Check a few contexts with the targeting rules tester. The result should be the value Remote Config serves them.
There are three ways to do this:
- The dashboard, for an app with a few parameters. The config form and the targeting rules editor cover everything above.
- The Terraform provider, if you want the configuration as code from the start. The ConfigDirector Terraform provider has
configdirector_configfor the config andconfigdirector_config_targeting_rulesfor the rules of each environment. - The MCP server, for a template with many parameters. Connect the MCP server to your coding assistant, give it the exported template, 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 platform at a time. Pass the parameter's in-app default 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.
Initialize the client
Create one client when the app starts, with the client SDK key of the environment the build runs against, and initialize it. Where Remote Config needs setDefaults, fetchAndActivate, and a minimum fetch interval, ConfigDirector needs the client and one call:
import 'package:configdirector_flutter_client_sdk/configdirector_flutter_client_sdk.dart';
final client = ConfigDirectorClient(clientSdkKey: 'YOUR-CLIENT-SDK-KEY');
await client.initialize();
import com.configdirector.ConfigDirectorClient
val client = ConfigDirectorClient(applicationContext, "YOUR-CLIENT-SDK-KEY")
// A suspend function: call it from a coroutine.
client.initialize()
import ConfigDirector
let client = try ConfigDirectorClient(clientSDKKey: "YOUR-CLIENT-SDK-KEY")
await client.initialize()
import { createClient } from "@configdirector/client-sdk";
export const client = createClient("YOUR-CLIENT-SDK-KEY");
await client.initialize();
initialize returns when the first values arrive or after its timeout. If your users are often on poor connections, do not await it: reads return the in-code default value until values arrive, and then the real values, exactly as Remote Config does before the first activate.
Read a value
Each read takes the key and the in-code default value, and the default decides the type the value is read as:
// Before: remoteConfig.getBool('new_checkout')
final newCheckout = client.getValue('new_checkout', false);
// Before: remoteConfig.getInt('max_items')
final maxItems = client.getValue('max_items', 20);
// Before: remoteConfig.getBoolean("new_checkout")
val newCheckout = client.value("new_checkout", false)
// Before: remoteConfig.getLong("max_items")
val maxItems = client.value("max_items", 20)
// Before: remoteConfig["new_checkout"].boolValue
let newCheckout = client.value(for: "new_checkout", default: false)
// Before: remoteConfig["max_items"].numberValue
let maxItems = client.value(for: "max_items", default: 20)
// Before: getBoolean(remoteConfig, "new_checkout")
const newCheckout = client.getValue("new_checkout", false);
// Before: getNumber(remoteConfig, "max_items")
const maxItems = client.getValue("max_items", 20);
A JSON parameter that you parsed from getString is read as JSON directly: pass an object or a map as the default and the SDK returns the parsed value.
Follow changes
Remote Config's real-time listener tells you a new template exists, and you call activate and re-read. ConfigDirector's watch, and the streams and flows of the mobile SDKs, give you the value itself, first the current one and then every change:
// Before: remoteConfig.onConfigUpdated.listen((event) async { await remoteConfig.activate(); ... })
final subscription = client.watch('new_checkout', false).listen((newCheckout) {
// Rebuild with the new value
});
// Before: remoteConfig.addOnConfigUpdateListener(...) then remoteConfig.activate()
lifecycleScope.launch {
client.values("new_checkout", false).collect { newCheckout ->
// Update the UI with the new value
}
}
// Before: remoteConfig.addOnConfigUpdateListener { ... remoteConfig.activate() }
for await newCheckout in client.values(for: "new_checkout", default: false) {
// Update the UI with the new value
}
// Before: onConfigUpdate(remoteConfig, ...) then activate(remoteConfig)
client.watch("new_checkout", false, (newCheckout) => {
// Render with the new value
});
In Flutter, pair watch with a StreamBuilder; in Jetpack Compose, use configValue from the Compose artifact; in SwiftUI, read the stream in a task. Each SDK page shows the pattern for its framework.
Give the context what your rules need
Remote Config knew the country, language, and app version of each installation on its own. ConfigDirector knows the app name and version through the SDK's metadata, and everything else from the context your app provides. Set the traits your targeting rules use, and update the context when the user signs in or out:
await client.initialize(
const ConfigDirectorContext(
id: '12345',
traits: {'country': 'US', 'language': 'en', 'platform': 'ios'},
),
);
// On sign out: an anonymous context
await client.updateContext(const ConfigDirectorContext());
client.initialize(
ConfigDirectorContext.build {
id("12345")
trait("country", "US")
trait("language", "en")
trait("platform", "android")
},
)
// On sign out: an anonymous context
client.updateContext(ConfigDirectorContext.build { anonymous(true) })
await client.initialize(
context: ConfigDirectorContext(
id: "12345",
traits: ["country": "US", "language": "en", "platform": "ios"]
)
)
// On sign out: an anonymous context
await client.updateContext(ConfigDirectorContext(isAnonymous: true))
await client.initialize({
id: "12345",
traits: { country: "US", language: "en", platform: "web" },
});
// On sign out: an anonymous context
await client.updateContext({ anonymous: true });
A context given without an identifier is anonymous: the values are still evaluated for it, with a stable identifier derived from the SDK instance, and it is never stored or shown in the dashboard.
The SDK for each platform
| Firebase Remote Config SDK | ConfigDirector SDK |
|---|---|
Flutter (firebase_remote_config) | Flutter |
| Android | Android, for Kotlin and Java, with a Jetpack Compose artifact |
| Apple platforms | Swift |
| Web | JavaScript, React, Vue, Next.js, Nuxt |
| React Native (community library) | React Native |
| Server-side Remote Config | Node.js, Python, Java, .NET, Go |
| Unity, C++ | Not available |
Every platform also has an OpenFeature provider, if you prefer to code against the vendor-neutral OpenFeature API.
Verify, then remove Remote Config
Once a build 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 app 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 value you rolled out by percentile, the assignment starts afresh: a user who was in Remote Config's 10% is not necessarily in ConfigDirector's. Switch over at a point in the rollout where that is acceptable, or at 0% or 100%.
Keep Remote Config in the app until the versions that read it have faded from your user base, then remove the dependency and delete the parameters from the template. From then on, the config lifecycle tells you when each temporary config has settled and can be removed from the code too.