Migrating from Firebase Remote Config
Last updated 20 September 2026
Done in the order below, this never has a cutover. Both services run together for one release, the old one is removed after the new one has proved itself in production, and no step needs a rollback plan.
Before you start
Export what you have. The Firebase console will give you the current template as JSON, or the Remote Config REST API will:
curl -X GET \
"https://firebaseremoteconfig.googleapis.com/v1/projects/YOUR_PROJECT/remoteConfig" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-o remote-config.jsonRead the parameters object and, for each one, write down three things: the key, the type it is really meant to be, and its default value. Firebase stores everything as a string, so the second column is a judgement you are making now rather than reading off. Getting it right here is the whole point of the exercise.
Parameters nothing reads any more are common, and this is the moment they are cheapest to find. Search your codebase for each key before you recreate it. Migrating dead config is how a migration doubles in size.
Step 1: recreate the parameters as typed flags
Create a project in Klor, then one flag per parameter you are keeping. Choose the real type: bool, string, number or json. The type is fixed at creation and enforced when you publish, so a number flag can never carry a string the way a Firebase parameter can.
If you have more than a handful, the management API is faster than the dashboard, and the script is short enough to read:
const template = JSON.parse(await readFile('remote-config.json', 'utf8'))
for (const [key, parameter] of Object.entries(template.parameters)) {
const raw = parameter.defaultValue?.value ?? ''
const valueType =
raw === 'true' || raw === 'false' ? 'bool'
: raw !== '' && !Number.isNaN(Number(raw)) ? 'number'
: raw.trimStart().startsWith('{') ? 'json'
: 'string'
await fetch('https://klor.dev/api/v1/flags', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.KLOR_TOKEN}`, 'content-type': 'application/json' },
body: JSON.stringify({ key, valueType }),
})
}Inspect what that guessed before you trust it. A version string like 2.4 parses as a number and is almost certainly meant to be a string.
Step 2: set the values, per environment
Firebase has one template per project and conditions inside it. Klor has three environments per project, each configured separately, which is usually what the Firebase conditions were approximating. Set development and production independently; nothing is shared between them, and that is the point.
Conditions with percentage rollouts become rules with a rollout. Conditions on app version or platform become rules with conditions on appVersion and platform. Conditions on Firebase audiences have no equivalent, because that data lives in Firebase Analytics; those become an attribute you pass in your own context.
Step 3: publish, and read from both
Publish the environment. Nothing reaches an app until you do, so up to this point the migration has changed nothing.
Now read from both in the same release, preferring Klor and falling back to Firebase. This is the step that removes the risk: if Klor is unreachable or a key is missing, the app behaves exactly as it does today.
const klorValue = useFlag('max_items', SENTINEL)
const firebaseValue = Number(remoteConfig().getValue('max_items').asString()) || 25
// During the migration only. Remove the second half once Klor has been
// serving this value in production for a release.
const maxItems = klorValue === SENTINEL ? firebaseValue : klorValueShip that. Watch it for a release. The dashboard shows when each flag was last read, so you can confirm the app is genuinely reading from Klor rather than silently falling through on every launch.
Step 4: remove Firebase, in the order that is safe
- Confirm every migrated flag shows a recent read in the Klor dashboard. A flag that was never read is a flag whose fallback has been doing the work.
- Delete the fallback branch, leaving useFlag with an ordinary default.
- Ship that release and wait for adoption. Remember that old binaries still call Firebase, so the Firebase template has to keep working until those users update.
- Only then empty the Firebase template, and only then remove the SDK.
The third point is the one people get wrong. A shipped binary is frozen: deleting the Firebase parameters the day you deploy breaks every user who has not updated yet. If you have Klor gating in place by then, you can require the update first, which is a neat use of the thing you just migrated to.
Things that do not translate
- Firebase personalisation, which picks a value per user using Google’s models. Klor has no equivalent and is not going to.
- Conditions on Analytics audiences, unless you can express the same idea as an attribute you already know on the device.
- The Firebase console’s change history, which does not come with you. Klor starts its own audit log and snapshot history from your first publish.
- A/B test integration. If you were using Firebase A/B Testing on top of Remote Config, read the note on experimentation before you plan around it.