Manage feature availability in FlutterFlow by storing feature flags in a Firestore app_config document. Load the config once at app launch, cache it in App State, and wrap feature UI in Conditional visibility widgets that check the flag. Use the package_info_plus package to enforce minimum version gates. For gradual rollouts, assign users a 0-100 random rollout_bucket integer on registration and compare it to the feature's rollout_percentage in the config.
Remote Feature Flags for FlutterFlow Apps
Shipping a new feature to 100% of users immediately is risky — if there is a bug or unexpected user behavior, everyone is affected. Feature flags solve this by putting a runtime switch between your code and your users. Store the switch in Firestore, and you can turn a feature on or off for all users, a specific user segment, or a gradually expanding percentage — all without publishing a new app version. In FlutterFlow, feature flags are implemented as a Firestore document loaded at startup and cached in App State, with Conditional visibility widgets that check the flag values.
Prerequisites
- A FlutterFlow project with Firebase and Authentication configured
- Basic understanding of FlutterFlow App State variables and Conditional visibility
- A Firebase project with Firestore enabled
Step-by-step guide
Create the app_config Firestore document with feature flags
Create the app_config Firestore document with feature flags
In the Firebase console, open Firestore and create a collection called 'app_config'. Add a single document with the ID 'features'. This document stores all your feature flags as Boolean or Map fields. Create these fields: new_checkout_flow (Boolean, default false), dark_mode_v2 (Boolean, default true), ai_assistant (Boolean, default false), min_required_version (String, e.g., '2.1.0'), maintenance_mode (Boolean, default false), and rollouts (Map — containing sub-fields like new_checkout_flow_percentage: Integer 0-100). In FlutterFlow, import this as a Firestore document in your Firestore panel. You will bind these values to App State variables after loading them at startup.
Expected result: An app_config/features document exists in Firestore with all flag fields set to their default values. You can manually toggle them in the Firebase console to test feature visibility.
Load feature flags at app launch and cache in App State
Load feature flags at app launch and cache in App State
Add App State Boolean variables for each feature flag you want to control: isNewCheckoutEnabled, isAiAssistantEnabled, isMaintenanceMode, etc. Also add a String App State variable called 'minRequiredVersion'. On your Entry Page's initState, add a Backend Query action (one-time fetch, not real-time) targeting app_config/features. In the Backend Query's on-success callback, add Set App State actions for each variable, binding to the corresponding Firestore field. Critically, do this before any other navigation: check isMaintenanceMode first — if true, navigate to a maintenance screen instead of the normal app flow. Checking feature flags on every page is wasteful; loading once at launch and caching in App State is the correct pattern.
Expected result: App State reflects the Firestore flag values within 1 second of app launch. Toggling a flag in Firestore takes effect on the next app launch (or implement a refresh on foreground for near-real-time updates).
Gate feature UI with Conditional visibility widgets
Gate feature UI with Conditional visibility widgets
For each feature controlled by a flag, wrap its UI in a Conditional visibility widget. Select the widget or container that represents the feature, go to its Properties panel, and toggle the 'Visible' property to a Condition. Set the condition to: App State > isNewCheckoutEnabled equals true. This makes the widget completely invisible (and not rendered at all) when the flag is false. For feature flags that replace existing functionality (like a new checkout flow replacing the old one), show the new UI when the flag is true and the old UI when it is false — use two parallel containers with opposing conditions: one visible when true, one visible when false.
Expected result: Toggling a flag in the Firebase console and restarting the app shows or hides the corresponding UI. The flag controls exactly the intended feature without affecting other parts of the app.
Implement gradual rollout using user rollout buckets
Implement gradual rollout using user rollout buckets
Gradual rollout releases a feature to an increasing percentage of users. When a user registers, assign them a random integer between 0 and 99 and store it in their Firestore user document as 'rollout_bucket'. This value never changes — it stably assigns the user to a consistent cohort. In your feature flag document, store each feature's rollout percentage: new_checkout_flow_percentage: 25 means users with rollout_bucket 0-24 see the feature. In your App State initialization, add a comparison: isNewCheckoutEnabled = (userRolloutBucket < featureRolloutPercentage). To roll out to more users, increase the percentage in Firestore — no app update required.
1// Custom Action: assign rollout bucket on registration2// Run once during user registration flow3import 'dart:math';4import 'package:cloud_firestore/cloud_firestore.dart';5import 'package:firebase_auth/firebase_auth.dart';67Future<void> assignRolloutBucket() async {8 final uid = FirebaseAuth.instance.currentUser?.uid;9 if (uid == null) return;10 final userRef = FirebaseFirestore.instance.collection('users').doc(uid);11 final snap = await userRef.get();12 // Only assign if not already assigned13 if (snap.data()?.containsKey('rollout_bucket') == true) return;14 final bucket = Random().nextInt(100); // 0-9915 await userRef.update({'rollout_bucket': bucket});16}Expected result: New users receive a rollout_bucket integer between 0-99 on registration. Users with bucket 0-24 see the feature when rollout_percentage is 25. Increasing rollout_percentage to 50 automatically includes users 25-49.
Enforce minimum version requirements and maintenance mode
Enforce minimum version requirements and maintenance mode
After code export, add the package_info_plus package (version ^5.0.1) to pubspec.yaml. Create a Custom Action called 'checkAppVersion' that calls PackageInfo.fromPlatform() to get the current app version string, compares it to the minRequiredVersion App State variable using semantic version comparison, and if the app is below the minimum, shows a non-dismissible dialog with an 'Update App' button that calls launchUrl() to open the app store listing. For maintenance mode, in your Entry Page initState check the isMaintenanceMode App State variable immediately after the feature flag load — if true, navigate to a MaintenancePage with an estimated return time loaded from Firestore.
1// custom_actions/check_app_version.dart2import 'package:package_info_plus/package_info_plus.dart';34// Returns true if current version meets minimum requirement5// Version format: major.minor.patch (e.g., '2.1.0')6Future<bool> checkAppVersion(String minVersion) async {7 final info = await PackageInfo.fromPlatform();8 final current = info.version;9 return _compareVersions(current, minVersion) >= 0;10}1112int _compareVersions(String v1, String v2) {13 final parts1 = v1.split('.').map(int.tryParse).toList();14 final parts2 = v2.split('.').map(int.tryParse).toList();15 for (int i = 0; i < 3; i++) {16 final p1 = (i < parts1.length ? parts1[i] : 0) ?? 0;17 final p2 = (i < parts2.length ? parts2[i] : 0) ?? 0;18 if (p1 != p2) return p1.compareTo(p2);19 }20 return 0;21}Expected result: Setting minRequiredVersion to '999.0.0' in Firestore forces the update dialog to appear. Setting it back to '1.0.0' dismisses it. Toggling maintenance_mode to true shows the maintenance screen immediately on next launch.
Complete working example
1// feature_flags.dart — Load and evaluate feature flags from Firestore2// Call loadFeatureFlags() from Entry Page initState3// Use evaluateFlag() to check if a feature is enabled for the current user45import 'package:cloud_firestore/cloud_firestore.dart';6import 'package:firebase_auth/firebase_auth.dart';78class FeatureFlags {9 static FeatureFlags? _instance;10 static FeatureFlags get instance => _instance ??= FeatureFlags._();11 FeatureFlags._();1213 Map<String, dynamic> _flags = {};14 int _userRolloutBucket = 100; // Default: not in any rollout1516 bool get maintenanceMode => _flags['maintenance_mode'] == true;17 String get minRequiredVersion => _flags['min_required_version'] as String? ?? '1.0.0';1819 Future<void> load() async {20 final uid = FirebaseAuth.instance.currentUser?.uid;21 try {22 // Load feature flags23 final flagSnap = await FirebaseFirestore.instance24 .collection('app_config').doc('features')25 .get().timeout(const Duration(seconds: 3));26 _flags = flagSnap.data() ?? {};2728 // Load user's rollout bucket29 if (uid != null) {30 final userSnap = await FirebaseFirestore.instance31 .collection('users').doc(uid)32 .get().timeout(const Duration(seconds: 3));33 _userRolloutBucket =34 (userSnap.data()?['rollout_bucket'] as int?) ?? 100;35 }36 } catch (e) {37 // Use cached/default values on load failure38 print('Feature flags load failed: $e');39 }40 }4142 /// Check if a feature is enabled for the current user.43 /// Handles: simple Boolean flags, rollout percentage flags.44 bool isEnabled(String flagName) {45 final flag = _flags[flagName];46 if (flag == null) return false;47 if (flag is bool) return flag;48 if (flag is Map) {49 final enabled = flag['enabled'] as bool? ?? false;50 if (!enabled) return false;51 final rolloutPct = flag['rollout_percentage'] as int? ?? 0;52 return _userRolloutBucket < rolloutPct;53 }54 return false;55 }5657 /// Check if user is in a specific A/B test variant58 String? getVariant(String experimentName) {59 final experiment = _flags['experiments'];60 if (experiment is! Map) return null;61 final exp = experiment[experimentName];62 if (exp is! Map) return null;63 final variants = exp['variants'] as List?;64 if (variants == null || variants.isEmpty) return null;65 // Assign deterministically based on rollout bucket66 final variantIndex = _userRolloutBucket % variants.length;67 return variants[variantIndex] as String?;68 }69}7071// Convenience Custom Action for FlutterFlow72Future<void> loadFeatureFlags() async {73 await FeatureFlags.instance.load();74}7576// Check a Boolean feature flag77bool isFeatureEnabled(String flagName) {78 return FeatureFlags.instance.isEnabled(flagName);79}8081// Get A/B test variant for an experiment82String getExperimentVariant(String experimentName) {83 return FeatureFlags.instance.getVariant(experimentName) ?? 'control';84}Common mistakes
Why it's a problem: Checking feature flags with a Backend Query on every widget that uses them
How to avoid: Load the entire app_config/features document once at app launch, store all flag values in App State variables, and check App State in each widget's Conditional visibility. Single read at launch, zero reads per page after that.
Why it's a problem: Using App State to store feature flags without handling load failure
How to avoid: Define sensible safe defaults for each flag (most features on, maintenance mode off). On Firestore load failure, keep the current App State values unchanged rather than resetting to empty. Log the failure for monitoring.
Why it's a problem: Changing a user's rollout_bucket after initial assignment
How to avoid: Assign the rollout bucket once during registration and never change it. The assignRolloutBucket Custom Action should check if the field already exists before writing, as shown in the example code.
Why it's a problem: Storing the minimum required version as an integer instead of a semantic version string
How to avoid: Always store and compare version numbers as semantic version strings ('major.minor.patch'). Compare each component as an integer in sequence, as shown in the checkAppVersion Custom Action.
Best practices
- Load feature flags once at app launch and cache in App State — never fetch per-page or per-widget.
- Use rollout buckets (0-99) assigned at registration for gradual rollouts rather than user segments based on recency.
- Start every new feature rollout at 5% for 24 hours before increasing to 25%, 50%, and 100%.
- Always define safe default values for all flags so the app functions correctly when Firestore is unreachable.
- Document every feature flag in a comment inside the app_config document or a separate team wiki — abandoned flags accumulate and create confusion without documentation.
- Set a TTL: remove old flags from Firestore and the codebase within 2 weeks of a feature reaching 100% rollout. Dead flags become technical debt.
- Use separate app_config documents for different environments (app_config/features_dev, app_config/features_prod) so you can test flag changes in development without affecting production users.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm implementing feature flags for a FlutterFlow app using Firestore. Write a Dart FeatureFlags singleton class that: fetches an app_config/features Firestore document on load, handles both simple Boolean flags and Map flags with 'enabled' Boolean and 'rollout_percentage' Integer sub-fields, compares a user's rollout_bucket integer against rollout_percentage for gradual rollout evaluation, and caches results so subsequent calls do not re-fetch Firestore. Include a 3-second timeout fallback.
In my FlutterFlow project I have an App State Boolean variable called 'isNewCheckoutEnabled'. I want to show a 'New Checkout' Container when this is true and a 'Classic Checkout' Container when it is false. I also have a Backend Query on app_config/features that loads the feature flag at app launch. Walk me through: setting up the Backend Query on my splash page, mapping the Firestore 'new_checkout_flow' Boolean field to the App State variable, and configuring Conditional visibility on both checkout Containers.
Frequently asked questions
Can I update a feature flag and have it take effect immediately without users restarting the app?
Yes. Change the Backend Query on your Entry Page (or a separate app config Component) from 'One Time' to 'Real Time'. Now whenever the Firestore document changes, the App State variables update immediately and any Conditional widgets respond. The trade-off is one persistent Firestore listener per active user.
How is this different from Firebase Remote Config?
Firebase Remote Config is Google's purpose-built feature flag service with A/B testing, condition-based targeting, and a dedicated dashboard. It is more powerful than the Firestore approach but requires adding the firebase_remote_config package after code export. The Firestore approach described here is simpler to set up within FlutterFlow's visual editor and gives you full control of the data structure.
What is the difference between feature flags and user roles?
Feature flags control whether a feature exists for a set of users. User roles control what actions a user is permitted to take. Both can gate UI, but feature flags are temporary (removed when the feature is fully rolled out) while roles are permanent business logic.
Can I show different app store screenshots for users in the new checkout experiment?
App store screenshots are static — you cannot target them by A/B variant. However, you can use different App Store Connect product pages (iOS 15+) for different A/B groups if you set up product page optimization in App Store Connect.
How do I handle a feature flag that was set to false for a user who is mid-flow through the new feature?
Add an App State listener on the feature flag. If the flag turns off while the user is on a new-feature page, navigate them back to the classic equivalent page with a gentle message: 'This feature is temporarily unavailable.' Don't abandon users mid-checkout or mid-form.
How many feature flags can I have in one Firestore document?
A Firestore document has a 1MB size limit. Storing 1,000 simple Boolean flags would use roughly 50KB — well within limits. In practice, apps rarely need more than 20-50 active flags. Regularly remove flags for features that are 100% rolled out to keep the document clean.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation