Back to Tutorials

EAS Update vs Firebase Remote Config in Expo Apps

Once an Expo app is in the stores, there are three ways to change it: a new store build, an EAS Update, or a Firebase Remote Config value. They overlap just enough to cause trouble. Teams end up shipping config as code, code as config, or trying to push a native change over the air. This guide gives each tool one job, wires Remote Config into an app that already uses EAS Update, and ends with a decision matrix and a rollback plan.

September 18, 2026 14 min read Paddy B
ExpoReact NativeEAS UpdateFirebase Remote ConfigFeature Flags

The short version

Remote Config changes values. EAS Update changes JavaScript. Store builds change everything else. Ship new code dark with an EAS Update, then switch it on with a Remote Config flag. That way the risky step is a switch you can flip back in seconds. Keep every flag's default in your code, so the default always ships in the same bundle as the code that reads it. One catch before you start: the Firebase JS SDK doesn't support Remote Config in React Native. You need React Native Firebase, which is native code, so Remote Config itself arrives in a store build, not an update.

Versions used, as of September 18, 2026

Expo SDK 57 (expo-updates 57.0), EAS CLI 24.7, and React Native Firebase 26.4. Expo SDK 58 is in beta. I'll update this page if any of the commands change.

1. Three levers, three jobs

Each tool changes a different layer of a shipped app, and each one reaches users in a different way:

  • Store build (EAS Build, then EAS Submit): a new binary. It's the only way to change native code, native dependencies, permissions, icons, or the Expo SDK. It goes through review, and users install it whenever they get round to it. Once it's out, you can't take it back.
  • EAS Update: a new JavaScript bundle and assets for binaries you've already shipped. By default, expo-updates downloads it in the background when the app starts and runs it on the next launch. It only reaches builds with a matching runtime version.
  • Remote Config: key-value parameters the app fetches while it runs. It doesn't change any code. It changes which path through your existing code runs. You can target values by platform, app version, country, a random percentage of users, or signals your app sends.

That leaves one rule to remember. Remote Config can only choose between behaviors that are already in the bundle. EAS Update can add behaviors, but only in JavaScript. Anything native needs a build.

2. Runtime versions decide what an update can reach

Every build has a runtime version, a label for the native code inside it. eas update gives each bundle your project's current runtime version, and a build only accepts updates with exactly the same label. That's what stops JavaScript that expects a new native module from reaching a binary without it and crashing at launch.

How the label is worked out depends on the policy in your app config:

// app.json { "expo": { "runtimeVersion": { "policy": "fingerprint" } } }
  • appVersion uses your version string. It's easy to read, but it relies on you bumping the version every time native code changes. Forget once and an update goes out to binaries it doesn't fit.
  • fingerprint hashes everything that can affect the native build, such as native dependencies, config plugins, and native app config. The runtime version changes automatically when any of those change. You'll make builds more often, but an incompatible update becomes very unlikely.
  • nativeVersion combines the version with the build number.

If you're adding React Native Firebase, use fingerprint. Every @react-native-firebase/* package bundles the native Firebase SDKs, so even a patch upgrade is a native change. That's exactly the kind of change people forget to bump a version for. Before you publish to production, check that your project still matches the build you shipped:

eas fingerprint:compare --build-id <production-build-id> --environment production

If the fingerprints differ, you need a store build, not an update. Expo SDK upgrades always change the runtime too, and my SDK 57 upgrade guide covers that release path.

3. Remote Config needs React Native Firebase

The earlier tutorials in this series use the Firebase JS SDK for Auth, Firestore, and Storage, which all work in Expo without native code. Remote Config is different. Firebase's own environment support table marks Remote Config (and Analytics) as unsupported in React Native. The supported route is React Native Firebase, which wraps the native iOS and Android SDKs.

That has three consequences:

  • It won't run in Expo Go. You need a development build.
  • The first version of your app that uses Remote Config has to ship as a store build. No update can add it.
  • The Remote Config module requires @react-native-firebase/analytics as a peer dependency. From v26 it also needs the New Architecture, which is the only architecture current Expo SDKs support, so you already have it.
npx expo install @react-native-firebase/app @react-native-firebase/analytics @react-native-firebase/remote-config expo-build-properties
// app.json { "expo": { "ios": { "bundleIdentifier": "com.example.shop", "googleServicesFile": "./GoogleService-Info.plist" }, "android": { "package": "com.example.shop", "googleServicesFile": "./google-services.json" }, "plugins": [ "@react-native-firebase/app", ["expo-build-properties", { "ios": { "useFrameworks": "dynamic" } }] ] } }

Only the app module needs a plugin entry. Remote Config has no config plugin, and Analytics' plugin is optional and only covers advertising ID settings. React Native Firebase pulls in the Firebase iOS SDK with Swift Package Manager by default, which is why iOS needs dynamic frameworks. Run npx expo prebuild --clean and rebuild. A development build you installed before adding these packages won't contain them.

You don't have to move Auth or Firestore over. Remote Config doesn't need to know who's signed in, so the JS SDK can keep handling Auth and Firestore while React Native Firebase handles Remote Config.

Analytics collects data as soon as it's installed

Installing @react-native-firebase/analytics turns on automatic collection, which you then have to declare in your App Store privacy details and Play data safety form. If you only need it because Remote Config requires it, turn collection off in a firebase.json at the root of your app project, then rebuild:

{ "react-native": { "analytics_auto_collection_enabled": false } }

Conditions based on Analytics user properties, and A/B Testing, need collection turned on. Platform, version, country, percentage, and custom signal conditions don't.

4. Defaults in code, new values on the next launch

Give every Remote Config parameter a default in your code. That way the code that reads a flag and its default travel together, in the same bundle. An update that adds a flag also adds its default, and a device that has never fetched still does something sensible.

Firebase describes several loading strategies. The best fit for an app that uses EAS Update is to load new values for the next startup. At launch, activate whatever was fetched during the previous session, then fetch again in the background. Values never change under a user mid-session, and new ones take effect on the next launch, the same way expo-updates applies bundles.

// lib/remoteConfig.ts import { activate, fetchConfig, getBoolean, getRemoteConfig, getString, setCustomSignals, } from "@react-native-firebase/remote-config"; import * as Updates from "expo-updates"; export const DEFAULTS = { new_checkout_enabled: false, checkout_kill_switch: false, promo_banner_text: "", min_supported_version: "1.0.0", }; const rc = getRemoteConfig(); export async function initRemoteConfig() { rc.defaultConfig = DEFAULTS; rc.settings = { minimumFetchIntervalMillis: __DEV__ ? 60_000 : 12 * 60 * 60 * 1000, fetchTimeoutMillis: 10_000, }; // Sent with every fetch, so await it before fetching (see section 5) await setCustomSignals(rc, { channel: Updates.channel ?? "development", runtime_version: Updates.runtimeVersion ?? "development", }); // Apply what the previous session fetched. This reads from disk, not the network. await activate(rc); // Fetch for next launch; the current session keeps its values fetchConfig(rc).catch(() => {}); } // Values only change when activate() runs, so reading at render time is fine export const flags = { newCheckout: () => getBoolean(rc, "new_checkout_enabled"), checkoutKilled: () => getBoolean(rc, "checkout_kill_switch"), promoText: () => getString(rc, "promo_banner_text"), minSupportedVersion: () => getString(rc, "min_supported_version"), };

Call initRemoteConfig() before you hide the splash screen, for example in the same effect that waits for fonts. The 12-hour production interval is Firebase's default. Don't lower it for everyone, because Firebase throttles apps that fetch too often. For the few values that really have to change faster, use real-time updates.

Real-time updates for kill switches

With a real-time listener attached, the native SDK keeps a connection open, fetches changes as soon as you publish them, and tells you which keys changed. It doesn't activate them for you:

// hooks/useLiveFlag.ts import { useEffect, useState } from "react"; import { activate, getBoolean, getRemoteConfig, onConfigUpdate, } from "@react-native-firebase/remote-config"; export function useLiveFlag(key: string) { const rc = getRemoteConfig(); const [value, setValue] = useState(() => getBoolean(rc, key)); useEffect(() => { return onConfigUpdate(rc, { next: async (update) => { if (!update.getUpdatedKeys().has(key)) return; await activate(rc); setValue(getBoolean(rc, key)); }, error: () => {}, complete: () => {}, }); }, [key]); return value; } // In the checkout screen const checkoutKilled = useLiveFlag("checkout_kill_switch");

Two things to know. First, activate is all or nothing, so it also applies any other pending changes, not just the key you were waiting for. Second, the open connection costs battery and data, so use this hook on the few screens with a kill switch, not for every flag. Listeners share one connection, and it closes when the last one unsubscribes.

5. Target flags at channels and binaries

An EAS Update doesn't change the app version. Remote Config's built-in App version and Build number conditions compare against CFBundleShortVersionString/versionName and CFBundleVersion/versionCode, which only a store build changes. So those conditions can tell binaries apart, but not updates or channels.

Custom signals fill that gap. The setCustomSignals call in initRemoteConfig sends the build's channel and runtime version with every fetch, and you can write conditions against them in the Firebase console:

  • channel exactly matches preview: turn a flag on for internal testers before anyone else.
  • runtime_version exactly matches a fingerprint: turn a feature on only in binaries that contain the native code it needs.
  • App version is at least 2.4.0 (built in): does the same job as the runtime check, as long as you bump versions reliably.

Signals are sent with a fetch, so set them before you fetch, as the code above does. Firebase allows up to 100 signals per app instance, with values up to 500 characters. Keep them free of personal data. A channel name is fine, but an email address isn't.

6. What belongs in Remote Config

Good fits:

  • Kill switches for features that are already live.
  • Feature flags for code that's already in the bundle.
  • Limits the UI uses, like items per page or a retry count. If the server has to enforce a limit, enforce it there too.
  • Promo banners and short copy that changes without a code change.
  • A minimum supported app version (section 8).
  • Percentage rollouts and A/B tests.

Keep out of Remote Config:

  • Secrets. Every value is downloaded to every matching device, and anyone can read it there. EAS Build environment variables explains what's safe to ship in an app and what isn't.
  • Security decisions. A flag that hides an admin screen doesn't stop anyone calling your backend. Enforce access in Firestore rules and Cloud Functions, and use App Check against unofficial clients.
  • Per-user data or anything large. That's Firestore's job.
  • Values that must change within minutes for everyone, unless you've set up the real-time listener.

Never change what a key means

Old bundles keep reading a key long after you've moved on. If checkout_config switches from one JSON shape to another, every device still running the previous update gets a shape it can't parse. Add checkout_config_v2 instead, and treat JSON parameters as untrusted input:

export function readJson<T>(key: string, fallback: T, isValid: (v: unknown) => v is T): T { try { const parsed: unknown = JSON.parse(getString(getRemoteConfig(), key)); return isValid(parsed) ? parsed : fallback; } catch { return fallback; } }

7. Use EAS Update for JavaScript changes

Good candidates for an EAS Update: JavaScript bug fixes, layout and styling, copy that lives in components, new screens built from native modules the binary already has, image assets, and new flags with their defaults.

Publish to the preview channel first, then to production with a partial rollout:

# Internal testers first eas update --channel preview --environment preview --message "Fix basket total rounding" # Then 10% of production eas update --channel production --environment production --rollout-percentage=10 --message "Fix basket total rounding" # Check launches and crashes, then widen the rollout eas update:insights <update-group-id> eas update:edit <update-group-id> --rollout-percentage=50

Only one rollout can run per branch at a time, and you can't publish another update for the same runtime version until it ends. Finish it by setting the percentage to 100, or undo it with eas update:revert-update-rollout.

Always pass --environment

EAS CLI requires --environment for projects on SDK 55 and later, and it matters for Firebase. It decides which EAS environment variables get bundled into the update, including the EXPO_PUBLIC_FIREBASE_* values the JS SDK is configured from. Publish a production update with preview variables and production users start talking to your staging Firebase project. EAS Build environment variables covers the setup.

Updates also have to follow store rules. Apple's guideline 2.5.2 says apps may not download code that "introduces or changes features or functionality of the app". Apple's developer agreement makes an exception for interpreted code like a JavaScript bundle, as long as it doesn't change the app's primary purpose. That exception is why EAS Update is allowed at all. Fixes and small features are routine. Turning the app into something the reviewer never saw is not.

8. Use store builds for native changes

These always need EAS Build and a store release:

  • Adding or upgrading any package with native code, including every @react-native-firebase/* package.
  • Expo SDK upgrades.
  • New permissions and their usage descriptions.
  • App config that ends up in the native project: plugins, scheme, icons, splash screen, bundle identifier, entitlements.
  • A new GoogleService-Info.plist or google-services.json.

The EAS Build and Submit checklist covers that release path. Because users update binaries whenever they like, old versions hang around for months. When you really need people off an old binary, a minimum supported version in Remote Config does the job:

// lib/storeVersion.ts import * as Application from "expo-application"; import { flags } from "./remoteConfig"; function isOlder(current: string, minimum: string) { const a = current.split(".").map(Number); const b = minimum.split(".").map(Number); for (let i = 0; i < Math.max(a.length, b.length); i++) { const diff = (a[i] ?? 0) - (b[i] ?? 0); if (diff !== 0) return diff < 0; } return false; } export function needsStoreUpdate() { const current = Application.nativeApplicationVersion; if (!current) return false; return isOlder(current, flags.minSupportedVersion()); }

When it returns true, show a full-screen prompt that links to the store listing. With the next-launch strategy from section 4, a new minimum takes effect one launch after the device fetches it. If you need it faster, read it with the real-time hook. Save this for real breakage, like a backend change the old binary can't handle. Keep your backend accepting the previous version for as long as you can, because users won't all open the app, fetch, and update on the same day.

9. The release decision matrix

ChangeUseWhy
Fix a JavaScript crash or logic bugEAS UpdateJavaScript only, same runtime
Turn off a broken feature right nowRemote Config kill switchNo download or relaunch with a real-time listener
Change promo or banner textRemote ConfigIt's a value, not code
Change copy written inside a componentEAS UpdateIt's code
Add a screen that uses existing native modulesEAS Update, behind a flagShip it dark, turn it on separately
Roll out a new feature graduallyEAS Update + Remote Config percentageCode ships once; exposure is a setting
Show a feature to internal testers onlyRemote Config, channel signalSame bundle, different audience
Add or upgrade a React Native Firebase packageStore buildNative code
Add a permission or usage descriptionStore buildLives in Info.plist and the manifest
Upgrade the Expo SDKStore buildNew runtime version
Get users off a broken old binaryStore build + min_supported_versionOnly a build fixes native code; the flag gets people to install it
Change the shape of a Remote Config valueA new keyOlder bundles still read the old one

10. Ship dark, then flip the flag

Using both tools together looks like this:

  1. Build the feature behind new_checkout_enabled, with false as its default in DEFAULTS.
  2. Publish an EAS Update to preview. In Remote Config, add a condition where channel matches preview, set the flag to true for it, and test.
  3. Publish the same code to production. The flag is still off there, so nothing changes for users yet.
  4. Wait for adoption. A flag only does something on devices running the update that reads it. eas channel:insights --channel production --runtime-version <runtime> shows how many users have it.
  5. Turn the flag on for 10% of users with a User in random percentage condition. Watch crash rates and support email, then widen it to 100%.
  6. Clean up. Remove the old code path and the flag check in a later update. Delete the parameter only once hardly anyone is still running an older bundle.

Deleting a parameter isn't neutral

Bundles that still read a deleted parameter fall back to their in-code default, which here is false. Delete new_checkout_enabled while people are still on the update from step 3 and the new checkout turns off for them. Remove the code first, and delete the parameter last.

Flagged features still have to be reviewable. Apple's guideline 2.3.1 bans hidden or dormant features and asks for new features to be described in the review notes and reachable during review. If a flagged feature is in the build you submit, describe it in App Store Connect's Notes for Review and give the reviewer a way to see it.

11. Rollback and monitoring plan

Write these commands down before release day. Each tool rolls back at a different speed.

Remote Config: seconds to hours

The Firebase console's change history lists every published template, and you can roll back to any stored version. Firebase keeps up to 300. From the CLI:

firebase remoteconfig:versions:list --limit 5 firebase remoteconfig:rollback -v 41

A rollback publishes a new version containing the old values. Devices with a real-time listener get it within seconds. The rest get it on their next fetch and use it from the launch after that, which with the settings above can take 12 hours or more. That delay is why kill switches need the listener.

EAS Update: a launch or two

If the update is still rolling out, eas update:revert-update-rollout puts everyone back on the previous update. Otherwise, eas update:rollback republishes the update before it, or tells devices to run the bundle embedded in the binary if there's nothing earlier. Devices pick up a rollback like any other update: they download it on one launch and run it on the next.

expo-updates also covers the worst case. If an update crashes before its first screen renders, that device marks it as failed and goes back to the previous one. That only protects you during startup. Bugs that appear later in a session are yours to roll back.

Store builds: no rollback, only phased releases

You can't pull a binary back off users' devices. Use the App Store's seven-day phased release and a staged rollout on Google Play, which you can halt. If a native bug gets through, fix forward with a new build, then raise min_supported_version once the fix is live.

Tag crashes with the release context

A crash spike after you flip a flag could come from the flag or from the update that went out the day before. Tag every crash report with both:

import * as Updates from "expo-updates"; import { flags } from "./remoteConfig"; export function releaseTags() { return { update_id: Updates.updateId ?? "dev", runtime_version: Updates.runtimeVersion ?? "dev", channel: Updates.channel ?? "dev", new_checkout: String(flags.newCheckout()), }; } // e.g. Sentry.setTags(releaseTags()) after initRemoteConfig()

On the EAS side, eas update:insights <update-group-id> shows launches, crashes, and unique users for one update, and eas channel:insights shows adoption for a channel and runtime version.

EAS Update and Remote Config FAQ

Can I use Firebase Remote Config with the Firebase JS SDK in Expo?

No. Firebase's environment support table lists Remote Config as unsupported in React Native for the JS SDK. Use @react-native-firebase/remote-config in a development build instead. It also requires @react-native-firebase/app and @react-native-firebase/analytics to be installed.

Can I add Firebase Remote Config to a live Expo app with an EAS Update?

No. React Native Firebase includes native code, so the first version of your app that uses Remote Config has to ship as a store build. After that, EAS Updates can add new flags and the code that reads them.

Should I fix a bug with EAS Update or Remote Config?

If the fix is code, use EAS Update. If the bug is in a feature that sits behind a flag, switch the flag off in Remote Config first because that reaches users faster, then ship the actual fix with EAS Update.

Does an EAS Update change the app version that Remote Config conditions see?

No. Remote Config's App version and Build number conditions read the native version and build number, and only a store build changes those. To target channels or specific binaries, send custom signals such as the channel and runtime version from expo-updates.

12. Production checklist

  • runtimeVersion uses the fingerprint policy, or you bump the version reliably with appVersion.
  • eas fingerprint:compare against the production build before every production update.
  • Every eas update passes an --environment that matches its channel.
  • Remote Config shipped in a store build before any update reads it.
  • Every parameter has a default in code, and the app works offline on its first launch.
  • Values activated at launch and fetched in the background, with a 12-hour interval in production.
  • Real-time listeners only on screens with kill switches.
  • channel and runtime_version sent as custom signals before the fetch.
  • No secrets, per-user data, or access control in Remote Config.
  • No key ever changes meaning; JSON parameters are validated with a fallback.
  • New features ship dark and get turned on in stages.
  • Flag checks removed from code before their parameters are deleted.
  • Flagged features described in the App Review notes.
  • Crash reports tagged with update ID, runtime version, channel, and flag values.
  • Rollback commands for all three tools written down before release day.

Related tutorials

Need this built for you? I take on contract Expo and React Native work — see how I can help.

Support tutorials