After months of building features and shipping releases, you've probably realized that deploying to production is just the beginning. Remote config and feature flags give you the power to change behavior without waiting for app store reviews or forcing users to update. By the end of Day 85, you'll know how to toggle features remotely, roll out changes gradually, kill broken features instantly, and keep your app functional offline.
Why remote config matters in production
When I was working on an e-commerce platform, we shipped a payment gateway integration that broke for 15% of users in a specific region. The fix was ready, but the app store review took 4 hours. During those 4 hours, we lost revenue and trust. Remote config would have let us disable that payment method in seconds.
Remote config isn't just for fixes. It's about control. You can A/B test pricing, enable premium features for specific user segments, adjust API endpoints between environments, or change UI behavior based on device capability. Firebase Remote Config is the most straightforward solution for Flutter, but I'll also show you patterns that work with any backend.
Toggle features remotely
The simplest use case: turn a feature on or off without shipping code. Here's a realistic setup with Firebase Remote Config.
import 'package:firebase_remote_config/firebase_remote_config.dart';
class FeatureFlags {
static final _instance = FirebaseRemoteConfig.instance;
static Future<void> initialize() async {
await _instance.setConfigSettings(
RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(hours: 1),
),
);
// Set defaults in case fetch fails
await _instance.setDefaults(const {
'payment_v2_enabled': false,
'new_checkout_flow': false,
'show_promo_banner': true,
});
// Fetch latest values
await _instance.fetchAndActivate();
}
static bool isPaymentV2Enabled() {
return _instance.getBool('payment_v2_enabled');
}
static bool isNewCheckoutFlowEnabled() {
return _instance.getBool('new_checkout_flow');
}
static bool shouldShowPromoBanner() {
return _instance.getBool('show_promo_banner');
}
}
In your checkout screen, use it like this:
Widget buildCheckout() {
if (FeatureFlags.isPaymentV2Enabled()) {
return PaymentGatewayV2();
}
return PaymentGatewayV1();
}
The benefit here is immediate. You set a flag in Firebase Console, and within the next fetch interval (you control this), your app adapts. No App Store review, no user friction.
Roll out gradually
Shipping a major feature to 100% of users at once is risky. Gradual rollout means you catch bugs early with a small audience before affecting everyone.
Firebase Remote Config doesn't have built-in percentage rollouts, but you can implement it by combining user segments with random logic or server-side decisions. Here's a common pattern:
class RolloutManager {
static Future<bool> shouldEnableNewFeatureForUser(String userId) async {
final remoteConfig = FirebaseRemoteConfig.instance;
// Fetch rollout percentage (0-100)
final rolloutPercentage = remoteConfig.getInt('new_feature_rollout_percent');
// Hash the user ID to get a consistent value per user
final hashValue = userId.hashCode.abs() % 100;
return hashValue < rolloutPercentage;
}
}
// Usage
final enabled = await RolloutManager.shouldEnableNewFeatureForUser(userId);
if (enabled) {
showNewFeature();
} else {
showLegacyFeature();
}
You start with new_feature_rollout_percent: 5 in Firebase Console. Watch for crashes and user complaints for a few hours. Then bump it to 25, then 50, then 100. If something breaks at 25%, you dial it back to 0 instantly without shipping a patch.
This is especially powerful for backend-heavy features. On a ride-hailing platform I worked on, we rolled out a new matching algorithm to 10% of drivers first, monitored acceptance rates and cancellations, then expanded it. A feature that might have broken the entire system was deployed safely.
Kill switch for broken features
Sometimes a feature works in development and testing, but production throws a curveball. Network conditions differ, load patterns change, or a third-party API behaves unexpectedly.
A kill switch is a feature flag that lets you disable a feature instantly when it misbehaves. This is your emergency brake.
class CriticalFeatures {
static final _remoteConfig = FirebaseRemoteConfig.instance;
static bool isCacheServiceHealthy() {
return _remoteConfig.getBool('cache_service_healthy');
}
static bool isAnalyticsServiceHealthy() {
return _remoteConfig.getBool('analytics_service_healthy');
}
static Future<void> fetchDataWithFallback() async {
try {
if (!isCacheServiceHealthy()) {
// Skip cache, hit database directly
return fetchFromDatabase();
}
return await fetchFromCache();
} catch (e) {
// If cache fails unexpectedly, we can disable it via remote config
// next time the app fetches
return fetchFromDatabase();
}
}
}
Real scenario: on a SaaS platform, a new Redis caching layer had a memory leak under high load. We didn't catch it in staging because staging traffic was 100x lighter. When production traffic spiked, Redis crashed every 6 hours. Instead of rolling back the entire feature, we flipped cache_service_healthy: false in Firebase Console. The app fell back to database queries (slower, but functional) while the backend team debugged and fixed the leak. Users saw no downtime.
Cache values for offline use
Fetching remote config every app launch adds latency. And if the network is down, you need sensible defaults. The pattern is straightforward: fetch in the background, use cached values on the current session, and update aggressively in the background.
import 'package:shared_preferences/shared_preferences.dart';
class CachedRemoteConfig {
static const _cacheKey = 'remote_config_cache';
static final _remoteConfig = FirebaseRemoteConfig.instance;
static Future<void> initialize() async {
final prefs = await SharedPreferences.getInstance();
// Set in-memory defaults from last session
final cachedJson = prefs.getString(_cacheKey);
if (cachedJson != null) {
final cached = jsonDecode(cachedJson) as Map<String, dynamic>;
_applyDefaults(cached);
}
// Fetch fresh values in the background
_remoteConfig.fetchAndActivate().then((_) {
// Cache the fresh values
final freshValues = <String, dynamic>{};
for (final key in _remoteConfig.getAll().keys) {
freshValues[key] = _remoteConfig.getValue(key).asString();
}
prefs.setString(_cacheKey, jsonEncode(freshValues));
}).catchError((e) {
// Fetch failed, offline or network issue - use cached values
print('Remote config fetch failed: $e');
});
}
static void _applyDefaults(Map<String, dynamic> defaults) {
// Set these as in-memory defaults
// Implementation depends on your defaults structure
}
static bool getFeatureEnabled(String key, {bool defaultValue = false}) {
return _remoteConfig.getBool(key);
}
}
Key insight: don't block app startup waiting for remote config. Fetch asynchronously during the splash screen or first navigation. Use SharedPreferences as a fallback. This means your app starts fast and updates in the background.
Common mistakes
- Fetching too often. Every app open should not fetch remote config. Set a minimum fetch interval (1-24 hours depending on urgency). Firebase Remote Config enforces a 12-hour default to prevent abuse.
- Not testing offline scenarios. If your app only works with remote config and the network is down on first launch, you're in trouble. Always set sensible defaults in code.
- Forgetting to call fetchAndActivate(). Fetching doesn't activate new values automatically. You must call
fetchAndActivate()for changes to take effect. New app sessions see the updated values after activation. - Mixing too many flags. After a year, you might have 50 dormant flags cluttering your codebase. Periodically archive unused flags and clean up code branches.
- Not monitoring flag changes. Set up alerts in your analytics or monitoring tool so you know when a flag is triggered or when a rollout is happening. This prevents accidental damage.
- Using remote config for sensitive data. Remote config is visible to your users (they can inspect network traffic). Never store API keys, auth tokens, or secrets here. Use backend-specific config or secure vaults.
Key takeaways
- Remote config decouples feature behavior from app releases, letting you fix issues in seconds instead of hours.
- Use feature flags for gradual rollouts—start at 5%, monitor, then expand. This catches bugs with minimal blast radius.
- Implement kill switches for critical services so you can disable a misbehaving feature without shipping code.
- Cache remote config values locally and fetch in the background to maintain offline functionality and reduce startup latency.
- Set sensible defaults in code so your app never depends on a successful remote config fetch to function.
- Don't over-fetch. Respect Firebase's rate limits and your users' battery by fetching on a reasonable schedule.
- Archive and clean up old flags regularly to keep your codebase maintainable.






Comments
Loading comments…