# How to Manage Multiple App Versions with Different Features in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-45 min
- Compatibility: FlutterFlow Free+ (package_info_plus requires code export for version gating)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

### 2. 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).

### 3. 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.

### 4. 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.

```
// Custom Action: assign rollout bucket on registration
// Run once during user registration flow
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

Future<void> assignRolloutBucket() async {
  final uid = FirebaseAuth.instance.currentUser?.uid;
  if (uid == null) return;
  final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
  final snap = await userRef.get();
  // Only assign if not already assigned
  if (snap.data()?.containsKey('rollout_bucket') == true) return;
  final bucket = Random().nextInt(100); // 0-99
  await userRef.update({'rollout_bucket': bucket});
}
```

**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.

### 5. 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.

```
// custom_actions/check_app_version.dart
import 'package:package_info_plus/package_info_plus.dart';

// Returns true if current version meets minimum requirement
// Version format: major.minor.patch (e.g., '2.1.0')
Future<bool> checkAppVersion(String minVersion) async {
  final info = await PackageInfo.fromPlatform();
  final current = info.version;
  return _compareVersions(current, minVersion) >= 0;
}

int _compareVersions(String v1, String v2) {
  final parts1 = v1.split('.').map(int.tryParse).toList();
  final parts2 = v2.split('.').map(int.tryParse).toList();
  for (int i = 0; i < 3; i++) {
    final p1 = (i < parts1.length ? parts1[i] : 0) ?? 0;
    final p2 = (i < parts2.length ? parts2[i] : 0) ?? 0;
    if (p1 != p2) return p1.compareTo(p2);
  }
  return 0;
}
```

**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 code example

File: `custom_actions/feature_flags.dart`

```dart
// feature_flags.dart — Load and evaluate feature flags from Firestore
// Call loadFeatureFlags() from Entry Page initState
// Use evaluateFlag() to check if a feature is enabled for the current user

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

class FeatureFlags {
  static FeatureFlags? _instance;
  static FeatureFlags get instance => _instance ??= FeatureFlags._();
  FeatureFlags._();

  Map<String, dynamic> _flags = {};
  int _userRolloutBucket = 100; // Default: not in any rollout

  bool get maintenanceMode => _flags['maintenance_mode'] == true;
  String get minRequiredVersion => _flags['min_required_version'] as String? ?? '1.0.0';

  Future<void> load() async {
    final uid = FirebaseAuth.instance.currentUser?.uid;
    try {
      // Load feature flags
      final flagSnap = await FirebaseFirestore.instance
          .collection('app_config').doc('features')
          .get().timeout(const Duration(seconds: 3));
      _flags = flagSnap.data() ?? {};

      // Load user's rollout bucket
      if (uid != null) {
        final userSnap = await FirebaseFirestore.instance
            .collection('users').doc(uid)
            .get().timeout(const Duration(seconds: 3));
        _userRolloutBucket =
            (userSnap.data()?['rollout_bucket'] as int?) ?? 100;
      }
    } catch (e) {
      // Use cached/default values on load failure
      print('Feature flags load failed: $e');
    }
  }

  /// Check if a feature is enabled for the current user.
  /// Handles: simple Boolean flags, rollout percentage flags.
  bool isEnabled(String flagName) {
    final flag = _flags[flagName];
    if (flag == null) return false;
    if (flag is bool) return flag;
    if (flag is Map) {
      final enabled = flag['enabled'] as bool? ?? false;
      if (!enabled) return false;
      final rolloutPct = flag['rollout_percentage'] as int? ?? 0;
      return _userRolloutBucket < rolloutPct;
    }
    return false;
  }

  /// Check if user is in a specific A/B test variant
  String? getVariant(String experimentName) {
    final experiment = _flags['experiments'];
    if (experiment is! Map) return null;
    final exp = experiment[experimentName];
    if (exp is! Map) return null;
    final variants = exp['variants'] as List?;
    if (variants == null || variants.isEmpty) return null;
    // Assign deterministically based on rollout bucket
    final variantIndex = _userRolloutBucket % variants.length;
    return variants[variantIndex] as String?;
  }
}

// Convenience Custom Action for FlutterFlow
Future<void> loadFeatureFlags() async {
  await FeatureFlags.instance.load();
}

// Check a Boolean feature flag
bool isFeatureEnabled(String flagName) {
  return FeatureFlags.instance.isEnabled(flagName);
}

// Get A/B test variant for an experiment
String getExperimentVariant(String experimentName) {
  return FeatureFlags.instance.getVariant(experimentName) ?? 'control';
}
```

## Common mistakes

- **Checking feature flags with a Backend Query on every widget that uses them** — If 10 widgets on a page each run a separate Firestore fetch to check whether a feature is enabled, that is 10 network calls per page load for data that changes rarely. This dramatically slows page loading and increases Firestore read costs. Fix: 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.
- **Using App State to store feature flags without handling load failure** — If the Firestore fetch fails (offline user, network error), App State variables remain at their initial values (all false/empty). This could hide features that should be visible or show maintenance mode when the app is actually working. Fix: 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.
- **Changing a user's rollout_bucket after initial assignment** — If the rollout bucket changes, a user who was seeing the new feature suddenly loses access, or a user who was not seeing it suddenly gets it. This creates inconsistent experiences and confuses users who are mid-flow in the new UI. Fix: 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.
- **Storing the minimum required version as an integer instead of a semantic version string** — Version '2.10.0' as an integer could be stored as 2100 or '210' — this creates comparison errors. Versions like '1.0.10' become '1010' which sorts incorrectly against '1.0.9' stored as '109'. Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-manage-multiple-app-versions-with-different-features-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-manage-multiple-app-versions-with-different-features-in-flutterflow
