How to migrate from Firebase Remote Config
Get your template out of Firebase with the console, CLI, or REST API, then bring every parameter into BetterConfig with its type, description, and per-environment values. A step-by-step guide with the gotchas labeled.
This guide moves your Firebase Remote Config parameters into BetterConfig. When you finish, every parameter exists as a typed key in BetterConfig (boolean, number, string, object, or array), each with its description and a value per environment, and your app reads that config over plain HTTPS from any language. No SDK is required to read.
The one mental-model shift to get right up front: Firebase has no first-class environments. The official guidance is one Firebase project per environment, so your dev, staging, and prod each live in a separate Firebase project with its own template. BetterConfig flips that: one project holds many environments. So the migration is one export per Firebase project, then one import into the matching BetterConfig environment.
If you are still deciding whether to move, read the BetterConfig vs Firebase Remote Config comparison and the Firebase Remote Config limitations reference first. This page assumes you have decided to migrate.
Step 1: Export your template (one per Firebase project)
A Remote Config template is a single JSON document. You can pull it three ways. Do this once for each Firebase project you treat as an environment.
From the console
Open Remote Config in the Firebase console and go to the Parameters or Conditions page. Open the three-dot menu in the top right and choose Download current config file. You get the live template as a .json file.
With the Firebase CLI
The Firebase CLI exposes the template through remoteconfig:get:
firebase remoteconfig:get -o prod.json --project your-prod-projectThat writes the current template to prod.json. To grab a specific historical version instead of the live one, add -v (or --version-number) with the version number.
With the REST API
If you script your exports, call the REST endpoint directly. You need an OAuth token with the https://www.googleapis.com/auth/firebase.remoteconfig scope; the snippet below mints one with the gcloud CLI.
curl --compressed \
-H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
-X GET \
"https://firebaseremoteconfig.googleapis.com/v1/projects/your-prod-project/remoteConfig" \
-o prod.jsonHowever you export it, the file has the same shape. Here is a trimmed example so you recognize the structure BetterConfig reads:
{
"conditions": [
{ "name": "ios_users", "expression": "device.os == 'ios'" }
],
"parameters": {
"checkout_v2_enabled": {
"defaultValue": { "value": "false" },
"valueType": "BOOLEAN",
"description": "Routes checkout through the rebuilt flow.",
"conditionalValues": {
"ios_users": { "value": "true" }
}
},
"max_upload_mb": {
"defaultValue": { "value": "50" },
"valueType": "NUMBER",
"description": "Per-file upload ceiling in megabytes."
}
},
"parameterGroups": {
"checkout": {
"parameters": {
"checkout_banner_text": {
"defaultValue": { "value": "" },
"valueType": "STRING"
}
}
}
},
"version": { "versionNumber": "42" }
}The pieces that matter are the parameters object, where each key carries a defaultValue.value, a valueType, an optional description, and any conditionalValues. Parameters can also be nested inside parameterGroups. The importer reads all of those.
Step 2: Know what imports and what doesn't
BetterConfig auto-detects the file. If parameter entries carry any of defaultValue, valueType, conditionalValues, or description, it treats the file as a Firebase template; otherwise it parses the file as a flat JSON object of key/value pairs. Here is exactly what crosses over.
What imports:
- The default value, converted by its declared type. A
BOOLEANparameter must hold the literal"true"or"false". ANUMBERmust parse to a finite number. AJSONvalue must parse and fit the type system. Anything else imports as a string. This matters because Firebase stores every value as a string and treats the data type as validation only, so BetterConfig does the coercion Firebase never did. - The description, truncated at 500 characters.
- Parameters inside groups. Group members import as normal keys; the group name itself is not preserved.
What is skipped, and why:
- Conditional values. The parameter still imports with its default value, but its
conditionalValuesare dropped. The preview shows one warning that conditional values are not supported and only defaults import. Step 4 covers what to do with them. - Parameters with no real default. A parameter set to
useInAppDefault: true, or one with no default value at all, is skipped with a per-key reason. There is nothing concrete to store. - Values that fail their type. A
BOOLEANthat is not the stringtrueorfalse, aNUMBERthat is not finite, or aJSONvalue that cannot be represented (a top-levelnullor a mixed-primitive array) is skipped with a reason. - Keys with names BetterConfig cannot accept. Keys must be 1 to 120 characters and start and end with a letter or digit; dots, underscores, and hyphens are allowed in the middle. Firebase allows up to 256 characters and a leading underscore, so leading-underscore keys and keys longer than 120 characters are skipped and listed individually so you can rename them.
One more limit to plan around: an import file holds at most 500 keys. Firebase caps a project at 3,000 parameters, so a maxed-out template is at most six files. If your template is larger than 500 keys, split it into files of up to 500 keys each and run the import once per file. Re-uploading one oversized file will not pick up the overflow; splitting is the supported path. The file itself must be under 10 MB.
| Firebase field | Imports? | Notes |
|---|---|---|
| defaultValue.value | Yes | Coerced to the declared BOOLEAN / NUMBER / JSON type, else stored as a string. |
| description | Yes | Truncated at 500 characters. |
| parameterGroups members | Yes | Imported as flat keys; the group name is dropped. |
| conditionalValues | No | Dropped with one warning. The default still imports. |
| useInAppDefault / no default | No | Skipped with a per-key reason. |
| Leading-underscore or >120-char keys | No | Skipped and listed so you can rename them. |
Step 3: Import, environment by environment
With your files ready, the import flow is the same for each one. Importing requires the editor role, and nothing is written until you confirm.
- Open the project, go to the Keys page, and click Import.
- Drop one exported file into the dialog.
- Read the preview. It lists every key with its inferred type, flags keys that already exist as conflicts, and surfaces the skip reasons from Step 2.
- Pick the target environment.
- Choose a conflict mode: skip (the default) leaves existing keys alone, overwrite updates them.
- Confirm.
The first import into a new project creates the keys. A new key gets your imported value in the environment you picked; every other environment starts at the type's default. So import your production template first, into the prod environment. Then import the staging template into the staging environment with overwrite, then dev into dev, and so on. Each pass sets that one environment's values.
Overwrite is safe to re-run. It appends a new version in the target environment only, so history stays intact and rollback is still available. A value that already matches the live one is skipped as unchanged, which makes a repeat import idempotent. An import never changes a key's existing type; a value that does not fit the current type is skipped with a type_mismatch reason rather than silently widening it.
Plan note. The Free plan gives you one environment, so on Free you migrate one Firebase project, usually production. Mirroring several Firebase projects as separate environments needs unlimited environments, which start on Pro at $19/month. This is a real constraint, not a soft nudge: if you run dev, staging, and prod as three Firebase projects today, plan for Pro to keep that shape.
Step 4: Decide what to do with conditional values
The importer does not convert conditions, and it never will pretend to. BetterConfig has no targeting engine today, so Firebase conditions, which target a group of app instances by platform, version, audience, percentage, or geo, have no automatic equivalent. Sort your conditional parameters into three honest buckets.
Bucket 1: conditions that were really environments in disguise. If a condition keys off an app id or a build variant, it was standing in for an environment. Recreate that value in the matching BetterConfig environment. Or preprocess the exported JSON before import: copy the relevant entry from conditionalValues into defaultValue, then import that edited file into that environment. The default is what crosses over, so this lands the right value in the right place.
Bucket 2: static platform or locale variants. If a parameter just splits by platform or language, fold the variants into the JSON value itself and branch on them in code. For example, store a single object key:
{
"ios": { "min_build": 412, "force_update": false },
"android": { "min_build": 388, "force_update": false }
}Then read the entry for the current platform off that one key in your app. One key, both variants, no targeting engine needed.
Bucket 3: true runtime targeting. Percentage rollouts, Analytics audiences, and anything that has to be evaluated per request belongs here. There is no BetterConfig equivalent today. Keep those few parameters in Firebase during the transition, or implement client-side bucketing yourself. Targeting is on the BetterConfig roadmap; until it ships, do not pretend the importer handles it.
Step 5: Point your app at the read API
Once your keys are imported and published, your app reads them from the edge-cached read API on Cloudflare Workers. Create a read-only token in Settings under API tokens (scope it to one environment or to the whole project), then make a single authenticated GET:
curl "https://api-public.betterconfig.dev/v1/config?project=acme-web&env=prod" \
-H "Authorization: Bearer $BC_READ_TOKEN"The response is a small JSON document:
{
"projectSlug": "acme-web",
"environmentKey": "prod",
"version": 3,
"fetchedAt": 1749600000000,
"values": {
"checkout_v2_enabled": false,
"max_upload_mb": 50,
"checkout_banner_text": ""
}
}Note the difference from Firebase's fetch model. The response sends an ETag of "v<version>" and Cache-Control: public, max-age=30, stale-while-revalidate=300, and it honors If-None-Match with a 304. Published changes are typically live at the edge in under a minute. That is the contrast worth naming against Firebase, whose default and recommended production minimum fetch interval is 12 hours: a value you change in Firebase may not reach clients for half a day, where a published BetterConfig value is typically live at the edge in under a minute.
For wiring this into a real app, the React and Next.js integration guide shows a Context pattern and a plain client-only fetch. For the full import reference, including every skip reason and the conflict modes, see the import documentation.
Rollback safety
Migrations go sideways, so the safety story matters. Every overwrite import appends a new version in the target environment, and BetterConfig keeps full per-environment history with one-click rollback. There is no retention cap on any plan, including Free, so an early test import does not age out and bury the version you want to return to.
Because reads are plain HTTPS, you can run Firebase and BetterConfig in parallel during the cutover. Point a slice of traffic, a single screen, or a staging build at the BetterConfig read API, confirm the values match, then move the rest over. If anything looks wrong, roll back the BetterConfig environment or fall back to Firebase while you investigate. Nothing about the import forces a hard switch.
One honest caveat to carry into the decision: where Firebase reads are free and uncapped, BetterConfig meters reads at the account level across all your projects, and going over the plan cap returns a 429 with {"error":"reads_limit_exceeded"}. It is a hard cap, never an overage charge; upgrading unblocks reads immediately and the cap resets at the start of each month. Size your plan against your real read volume before you cut traffic over. If you are still weighing tools, the ConfigCat vs Flagsmith comparison covers two flag platforms with different pricing models worth a look.