Muhammad Uzair
Firebase

Flutter Firebase Remote Config: Feature Flags Done Right

Published: 2026-10-11 · 7 min read

Master Flutter Firebase Remote Config: safe defaults, a typed wrapper, live updates, staged rollouts, and kill switches. Change behavior without a new build.

Every mobile developer knows the feeling: you spot a bug in production, or you want to turn off a half-baked feature, and the fix is a three-day app store review away. Flutter Firebase Remote Config exists to break that cycle. It lets you change app behavior — kill switches, rollout percentages, copy, paywall gating — from the Firebase console with zero new builds. Used well, it's the closest thing to a server-side feature flag system you'll get for free.

What Remote Config Is (and Isn't)

Remote Config is a key-value store that your app fetches at runtime. You define parameters in the Firebase console (e.g. new_checkout_enabled = true, promo_banner_text = "Summer sale"), and the app downloads them on launch. Crucially, it is not a realtime database and not a replacement for an API: values are cached, fetches are throttled by Firebase, and the intended use is configuration, not user data. Think feature flags, A/B test variants, maintenance-mode switches, and seasonal copy — not chat messages or live prices.

One more critical property: every parameter has a default value bundled in the app. If the fetch fails — airplane mode, Firebase outage, first launch with no network — the app falls back to the default. Your defaults must always represent the safe behavior, because they are what runs when everything else fails.

Step 1: Add the Dependency and Define Defaults

Add the package:

dependencies:
  firebase_core: ^x.y.z
  firebase_remote_config: ^x.y.z

Define your defaults in code (or a JSON asset). Keep the key names in one place — a constants file or the service class below — so a typo in a key string can't silently return the wrong default:

const Map<String, dynamic> _defaults = {
  'new_checkout_enabled': false,
  'promo_banner_text': 'Welcome back!',
  'max_free_uploads': 5,
  'maintenance_mode': false,
};

The principle: defaults describe the app as if Remote Config didn't exist. New features default to off. Limits default to the conservative value. If someone ships the app with fetch completely broken, nothing dangerous happens.

Step 2: Fetch, Activate, and Build a Typed Wrapper

Raw Remote Config access is stringly-typed and easy to misuse, so wrap it in a small service. This is the pattern I use on every project:

import 'package:firebase_remote_config/firebase_remote_config.dart';

class RemoteConfigService {
  RemoteConfigService._();
  static final RemoteConfigService instance = RemoteConfigService._();

  late final FirebaseRemoteConfig _config;

  Future<void> init() async {
    _config = FirebaseRemoteConfig.instance;

    await _config.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 30),
      // 1 hour in production; shorter in dev/staging
      minimumFetchInterval: const Duration(hours: 1),
    ));

    await _config.setDefaults(_defaults);

    try {
      await _config.fetchAndActivate();
    } catch (_) {
      // Offline or throttled — defaults are already in place.
    }
  }

  bool get newCheckoutEnabled =>
      _config.getBool('new_checkout_enabled');

  String get promoBannerText =>
      _config.getString('promo_banner_text');

  int get maxFreeUploads =>
      _config.getInt('max_free_uploads');

  bool get maintenanceMode =>
      _config.getBool('maintenance_mode');
}

Initialize it in main() after Firebase setup:

await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await RemoteConfigService.instance.init();

Then use it like any other config source:

if (RemoteConfigService.instance.maintenanceMode) {
  return const MaintenanceScreen();
}

A few things worth noticing in the wrapper. First, fetchAndActivate() in one call — the older two-step fetch-then-activate pattern still exists, but for app launch you want the value ready immediately. Second, the try/catch around the fetch: a failed fetch is a normal, expected event, not an error to report. Third, minimumFetchInterval of one hour — Firebase throttles fetches aggressively, and hammering the fetch endpoint gets you rate-limited with no benefit, because values only change when you publish them.

Real-Time Config Updates

One limitation of the fetch model: the app only picks up new values when it fetches. For kill switches that need to take effect now — say you need to disable a broken feature while users have the app open — Remote Config supports real-time listeners:

_config.onConfigUpdated.listen((event) async {
  await _config.activate();
  // Notify the UI: use a ValueNotifier, a state management
  // event, or simply rebuild the relevant widget.
});

Wire this up once at startup. The stream fires when you publish new values in the console, activate() applies them, and your UI reacts. This turns Remote Config from "checked at launch" into a genuine remote control for the app. Don't overuse it — most parameters are fine with launch-time fetching — but for kill switches and maintenance mode, real-time is the right tool.

Staging vs. Production and Safe Rollouts

Remote Config values are per Firebase project, not per app flavor — unless you set up conditions. The practical setup:

  • Use conditions in the console to target values by app version, platform, or a custom signal. A common pattern: new_checkout_enabled = true for app versions >= 2.4.0, false otherwise — so old app versions that don't understand the flag never see it.
  • Keep separate Firebase projects for staging and production. Your staging builds point at the staging project, where you test new flags freely; production values only change deliberately.
  • For gradual rollouts, use the random percentile condition: enable the feature for 5% of users, watch crash-free metrics for a day, then raise to 25%, 50%, 100%. This is the single most valuable Remote Config pattern for risky launches — it's a poor man's staged rollout without a new build.

And one rule I never break: changing a Remote Config value in production is a deployment. It deserves the same care as a code push — announce it, watch the dashboards, and know how to revert (publishing the previous value takes seconds, which is exactly why flags beat builds for emergencies).

Practical Patterns That Earn Their Keep

Kill switches. Every risky feature gets a boolean flag defaulting to off or on-as-shipped, with the dangerous direction covered. Payment flow acting up? Flip payments_enabled to false and the app shows a graceful "temporarily unavailable" instead of charging users twice.

Paywall and monetization gating. Gate premium features behind flags so you can run pricing experiments or disable a broken paywall flow instantly. Combine with the percentile condition for A/B tests on price points.

Maintenance mode. A single maintenance_mode boolean that swaps the whole app to a friendly "we'll be right back" screen. Pair it with a maintenance_message string parameter so the copy is editable without a release.

Seasonal and marketing copy. Promo banners, onboarding headlines, empty-state messages — anything marketing might want to change on a Friday afternoon. Every one of these is a release you don't have to cut.

Pitfalls That Bite Everyone Once

  • Unsafe defaults. The classic: defaulting new_feature_enabled to true, then discovering the fetch fails on a chunk of devices and the half-finished feature is live for everyone. Defaults must always be the safe state.
  • Fetching inside build(). Fetch once at startup (and on real-time updates), never in a widget's build method. Fetch is async, throttled, and not free.
  • Throttling confusion. During development, Firebase's minimum fetch interval makes it look like your console changes "aren't working." Drop minimumFetchInterval to minutes in dev builds, keep it at an hour or more in production.
  • Type mismatches. getBool on a parameter defined as a string in the console throws. Keep types consistent between console and code, and prefer the typed wrapper so mismatches surface in one place.
  • Forgetting old app versions. A flag that means one thing in v2.4 and another in v2.1 is a bug factory. Use version conditions, and delete flags for features that shipped to 100% months ago — flag debt is real.

Remote Config won't replace good release discipline, but it compresses the distance between "we need to change this" and "it's changed" from days to seconds. Start with one flag — a kill switch for your riskiest feature — and you'll wonder how you shipped without it.

Want feature flags, staged rollouts, and a proper release pipeline wired into your Flutter app? Get in touch — I set this up on production apps regularly, and the first kill switch usually pays for the whole engagement.

Frequently Asked Questions

How do I set up Firebase Remote Config in Flutter?

Define parameters in the Firebase console, set defaults in the app, call fetchAndActivate() once at startup, and expose values through a typed wrapper service. A failed fetch is a normal event — the app falls back to the bundled defaults, so wrap the fetch in try/catch.

Is Remote Config a realtime database?

No. Remote Config is a cached key-value store for configuration, not a realtime database and not a replacement for an API. Fetches are throttled, values update only when published, and it is meant for feature flags, A/B test variants, maintenance-mode switches, and copy — not user data or live prices.

How do I do a gradual feature rollout with Remote Config?

Use console conditions: target by app version so old versions never see flags they do not understand, and use the random percentile condition for staged rollouts — enable for 5% of users, watch crash-free metrics for a day, then raise to 25%, 50%, and 100%.

How do I make Remote Config kill switches take effect immediately?

Listen to onConfigUpdated at startup and call activate() when it fires, then notify the UI. This makes kill switches and maintenance mode take effect immediately without a relaunch. Most parameters are fine with launch-time fetching, but real-time is the right tool for kill switches.

Related Articles

← Back to all articles