RORK LABJP
TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer accountCLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch inputBROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode requiredPUBLISH — Two-click App Store publishing keeps the submission process simpleMAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision ProRN — Standard Rork generates iOS and Android apps together with React Native (Expo)TEST — The Rork Companion app lets you test on a real iPhone without a paid Apple Developer accountCLOUD — Code compiles on a cloud Mac, streaming a 60fps live simulator with real touch inputBROWSER — Design, code, and test entirely in Chrome or Safari — no Xcode requiredPUBLISH — Two-click App Store publishing keeps the submission process simpleMAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, and Vision ProRN — Standard Rork generates iOS and Android apps together with React Native (Expo)
Articles/Dev Tools
Dev Tools/2026-03-22Advanced

Feature Flags and Remote Config in Rork Max

Learn how to implement Feature Flags and Remote Config in your Rork Max app for safe gradual rollouts, A/B testing, and instant kill switches using Firebase.

Rork Max189Feature Flags2Remote Config6A/B Testing7Gradual RolloutFirebase10

Why Feature Flags and Remote Config Are Game-Changers

Shipping a new feature to every user at once is risky. If something breaks, you're stuck waiting for App Store review while your entire user base suffers. Feature Flags and Remote Config solve this problem by giving you server-side control over what users see — without deploying a new build.

Feature Flags let you toggle specific features on or off from a server dashboard. Remote Config lets you change app settings dynamically without requiring users to update. Together, they unlock powerful release strategies:

  • Roll out new features to a small percentage of users first (canary releases)
  • Run A/B tests comparing different UIs or copy
  • Instantly disable a broken feature (kill switch)
  • Update text, colors, and settings without an app review

Prerequisites and Setup

Before getting started, make sure you have:

  • An active Rork Max subscription
  • A Firebase project (the free Spark plan works fine)
  • Basic familiarity with React Native / Expo
  • Experience with native module integration in Rork Max (see the Native API Guide)

Setting Up Firebase

Create a Firebase project in the Firebase Console, then register your iOS and Android apps. If you're using Expo with Rork Max, add the Firebase config file paths to your app.json.

# Install Firebase packages
npx expo install @react-native-firebase/app @react-native-firebase/remote-config

Designing the Feature Flags Architecture

The Three-Layer Approach

A well-designed Feature Flags system consists of three layers:

  1. Remote Config Provider — Fetches config from Firebase and distributes it app-wide
  2. Feature Flag Hook — A custom hook for components to read flag values
  3. Flag Guard Component — A wrapper that conditionally renders UI based on flags

Implementing the Remote Config Provider

This provider fetches the latest config from Firebase at app startup and makes it available to every component via React Context.

// src/providers/RemoteConfigProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import remoteConfig from '@react-native-firebase/remote-config';
 
// Default values (fallbacks for offline or failed fetches)
const DEFAULT_FLAGS: Record<string, boolean> = {
  enable_new_onboarding: false,
  enable_dark_mode_v2: false,
  enable_ai_suggestions: false,
  enable_social_sharing: false,
};
 
const DEFAULT_CONFIG: Record<string, string> = {
  welcome_message: 'Welcome to the app!',
  max_upload_size_mb: '10',
  api_timeout_seconds: '30',
};
 
interface RemoteConfigState {
  flags: Record<string, boolean>;
  config: Record<string, string>;
  isLoading: boolean;
  lastFetchTime: Date | null;
}
 
const RemoteConfigContext = createContext<RemoteConfigState>({
  flags: DEFAULT_FLAGS,
  config: DEFAULT_CONFIG,
  isLoading: true,
  lastFetchTime: null,
});
 
export function RemoteConfigProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<RemoteConfigState>({
    flags: DEFAULT_FLAGS,
    config: DEFAULT_CONFIG,
    isLoading: true,
    lastFetchTime: null,
  });
 
  useEffect(() => {
    async function initRemoteConfig() {
      try {
        // Use a shorter cache interval in development
        await remoteConfig().setConfigSettings({
          minimumFetchIntervalMillis: __DEV__ ? 0 : 3600000, // Production: 1 hour
        });
 
        // Set default values
        await remoteConfig().setDefaults({
          ...DEFAULT_FLAGS,
          ...DEFAULT_CONFIG,
        });
 
        // Fetch and activate the latest values
        await remoteConfig().fetchAndActivate();
 
        // Read all values
        const allValues = remoteConfig().getAll();
        const flags: Record<string, boolean> = {};
        const config: Record<string, string> = {};
 
        Object.entries(allValues).forEach(([key, entry]) => {
          if (key in DEFAULT_FLAGS) {
            flags[key] = entry.asBoolean();
          } else {
            config[key] = entry.asString();
          }
        });
 
        setState({
          flags: { ...DEFAULT_FLAGS, ...flags },
          config: { ...DEFAULT_CONFIG, ...config },
          isLoading: false,
          lastFetchTime: new Date(),
        });
      } catch (error) {
        console.warn('Remote Config fetch failed, using defaults:', error);
        setState(prev => ({ ...prev, isLoading: false }));
      }
    }
 
    initRemoteConfig();
  }, []);
 
  return (
    <RemoteConfigContext.Provider value={state}>
      {children}
    </RemoteConfigContext.Provider>
  );
}
 
// Custom hook to read a feature flag
export function useFeatureFlag(flagName: string): boolean {
  const { flags } = useContext(RemoteConfigContext);
  return flags[flagName] ?? false;
}
 
// Custom hook to read a remote config value
export function useRemoteConfig(key: string): string {
  const { config } = useContext(RemoteConfigContext);
  return config[key] ?? '';
}
 
// Hook to check loading state
export function useRemoteConfigStatus() {
  const { isLoading, lastFetchTime } = useContext(RemoteConfigContext);
  return { isLoading, lastFetchTime };
}

The Flag Guard Component

A simple wrapper that renders children only when a flag is enabled:

// src/components/FeatureGate.tsx
import React from 'react';
import { useFeatureFlag } from '../providers/RemoteConfigProvider';
 
interface FeatureGateProps {
  flag: string;
  children: React.ReactNode;
  fallback?: React.ReactNode; // Alternative UI when flag is off
}
 
export function FeatureGate({ flag, children, fallback = null }: FeatureGateProps) {
  const isEnabled = useFeatureFlag(flag);
  return <>{isEnabled ? children : fallback}</>;
}
 
// Usage:
// <FeatureGate flag="enable_ai_suggestions">
//   <AISuggestionsPanel />
// </FeatureGate>
//
// Expected output:
// - enable_ai_suggestions is true → AISuggestionsPanel renders
// - enable_ai_suggestions is false → Nothing renders

Implementing A/B Testing

Integrating with Firebase A/B Testing

Firebase Remote Config has built-in A/B Testing that automatically segments users into groups and delivers different values. Here's how to leverage this in your Rork Max app:

// src/hooks/useABTest.ts
import { useEffect } from 'react';
import { useRemoteConfig } from '../providers/RemoteConfigProvider';
import analytics from '@react-native-firebase/analytics';
 
type ABTestVariant = 'control' | 'variant_a' | 'variant_b';
 
export function useABTest(testName: string): ABTestVariant {
  const variant = useRemoteConfig(`ab_${testName}`) as ABTestVariant;
  const resolvedVariant = variant || 'control';
 
  useEffect(() => {
    // Log which variant the user was assigned to
    analytics().setUserProperty(`ab_${testName}`, resolvedVariant);
    analytics().logEvent('ab_test_exposure', {
      test_name: testName,
      variant: resolvedVariant,
    });
  }, [testName, resolvedVariant]);
 
  return resolvedVariant;
}
 
// Usage: A/B testing the onboarding flow
// function OnboardingScreen() {
//   const variant = useABTest('onboarding_flow_2026');
//
//   switch (variant) {
//     case 'variant_a':
//       return <OnboardingCarousel />;     // Swipe-based
//     case 'variant_b':
//       return <OnboardingVideo />;        // Video-based
//     default:
//       return <OnboardingClassic />;      // Classic (control)
//   }
// }
//
// Expected output:
// - Users assigned to variant_a in Firebase Console → Carousel onboarding
// - Users assigned to variant_b → Video onboarding
// - Control group → Classic onboarding

Designing Conversion Tracking

Proper conversion tracking is critical for evaluating A/B test results accurately.

// src/utils/abTestTracking.ts
import analytics from '@react-native-firebase/analytics';
 
export const ABTestEvents = {
  // Track onboarding completion rate
  onboardingCompleted: (testName: string, variant: string) => {
    analytics().logEvent('onboarding_completed', {
      test_name: testName,
      variant,
      timestamp: Date.now(),
    });
  },
 
  // Track CTA button click-through rate
  ctaClicked: (testName: string, variant: string, ctaLabel: string) => {
    analytics().logEvent('cta_clicked', {
      test_name: testName,
      variant,
      cta_label: ctaLabel,
    });
  },
 
  // Track purchase conversions
  purchaseCompleted: (testName: string, variant: string, amount: number) => {
    analytics().logEvent('purchase', {
      test_name: testName,
      variant,
      value: amount,
      currency: 'USD',
    });
  },
};

Implementing Gradual Rollouts

Percentage-Based Rollouts

When rolling out a new feature incrementally, hashing the user ID to determine rollout eligibility is a reliable approach.

// src/utils/rollout.ts
import { useRemoteConfig } from '../providers/RemoteConfigProvider';
 
function hashStringToPercent(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32-bit integer
  }
  return Math.abs(hash) % 100;
}
 
export function useGradualRollout(
  featureName: string,
  userId: string
): boolean {
  // Get the rollout percentage from Remote Config (e.g., "25" = 25% of users)
  const rolloutPercentage = parseInt(
    useRemoteConfig(`rollout_${featureName}_percent`),
    10
  );
 
  if (isNaN(rolloutPercentage) || rolloutPercentage <= 0) return false;
  if (rolloutPercentage >= 100) return true;
 
  // Determine eligibility based on the hash of user ID
  const userPercent = hashStringToPercent(`${featureName}_${userId}`);
  return userPercent < rolloutPercentage;
}
 
// Usage:
// const isNewCheckoutEnabled = useGradualRollout('new_checkout', user.id);
//
// Setting rollout_new_checkout_percent = "10" in Firebase Console
// means roughly 10% of users will see the new checkout screen

Building a Kill Switch

When something goes wrong in production, you need the ability to disable a feature instantly.

// src/hooks/useKillSwitch.ts
import { useFeatureFlag } from '../providers/RemoteConfigProvider';
import { useEffect } from 'react';
 
export function useKillSwitch(featureName: string): {
  isKilled: boolean;
  message: string;
} {
  const isKilled = useFeatureFlag(`kill_${featureName}`);
  const message = 'This feature is temporarily unavailable. Please try again later.';
 
  useEffect(() => {
    if (isKilled) {
      console.warn(`[KillSwitch] ${featureName} is disabled`);
    }
  }, [isKilled, featureName]);
 
  return { isKilled, message };
}
 
// Usage:
// function PaymentScreen() {
//   const { isKilled, message } = useKillSwitch('payment');
//   if (isKilled) {
//     return <MaintenanceScreen message={message} />;
//   }
//   return <PaymentForm />;
// }
//
// Expected output:
// - kill_payment is false → Normal payment screen renders
// - kill_payment is true → Maintenance screen renders (no payment processing)

Wiring Everything Together

Adding the Provider to App.tsx

Place the provider at the root of your app so every component has access to feature flags and config.

// App.tsx
import React from 'react';
import { RemoteConfigProvider } from './src/providers/RemoteConfigProvider';
import { NavigationContainer } from '@react-navigation/native';
import { RootNavigator } from './src/navigation/RootNavigator';
 
export default function App() {
  return (
    <RemoteConfigProvider>
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </RemoteConfigProvider>
  );
}

Practical Usage Patterns

Here are common patterns for putting Feature Flags to work in your app:

// Toggle an entire screen
function HomeScreen() {
  const showNewHome = useFeatureFlag('enable_new_home_v2');
  return showNewHome ? <HomeScreenV2 /> : <HomeScreenV1 />;
}
 
// Toggle part of a screen
function ProfileScreen() {
  const showBadges = useFeatureFlag('enable_user_badges');
  return (
    <View>
      <UserInfo />
      {showBadges && <BadgeCollection />}
      <ActivityFeed />
    </View>
  );
}
 
// Dynamically update text via Remote Config
function PromotionBanner() {
  const bannerText = useRemoteConfig('promotion_banner_text');
  const bannerColor = useRemoteConfig('promotion_banner_color');
 
  if (!bannerText) return null;
 
  return (
    <View style={{ backgroundColor: bannerColor || '#FF6B35' }}>
      <Text>{bannerText}</Text>
    </View>
  );
}

Building a Debug Panel

A debug screen makes it easy to test Feature Flags during development without touching Firebase Console.

// src/screens/DebugFlagsScreen.tsx (dev builds only)
import React, { useState } from 'react';
import { View, Text, Switch, ScrollView, StyleSheet } from 'react-native';
import { useFeatureFlag } from '../providers/RemoteConfigProvider';
 
const FLAG_LIST = [
  { key: 'enable_new_onboarding', label: 'New Onboarding' },
  { key: 'enable_dark_mode_v2', label: 'Dark Mode v2' },
  { key: 'enable_ai_suggestions', label: 'AI Suggestions' },
  { key: 'enable_social_sharing', label: 'Social Sharing' },
];
 
export function DebugFlagsScreen() {
  const [overrides, setOverrides] = useState<Record<string, boolean>>({});
 
  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>Feature Flags Debug</Text>
      {FLAG_LIST.map(flag => {
        const remoteValue = useFeatureFlag(flag.key);
        const currentValue = overrides[flag.key] ?? remoteValue;
 
        return (
          <View key={flag.key} style={styles.row}>
            <View>
              <Text style={styles.label}>{flag.label}</Text>
              <Text style={styles.key}>{flag.key}</Text>
            </View>
            <Switch
              value={currentValue}
              onValueChange={val =>
                setOverrides(prev => ({ ...prev, [flag.key]: val }))
              }
            />
          </View>
        );
      })}
    </ScrollView>
  );
}
 
const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 16 },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  label: { fontSize: 16 },
  key: { fontSize: 12, color: '#888', marginTop: 2 },
});

A Note from an Indie Developer

Wrapping Up

Feature Flags and Remote Config fundamentally change how you ship software. By combining Rork Max's native API capabilities with Firebase Remote Config, you gain the ability to run gradual rollouts, A/B tests, and kill switches — all without waiting for App Store review.

Start small with a single flag, establish your team's naming conventions and lifecycle rules early, and build from there. The patterns in this guide scale well, and your future self will thank you for the safety net they provide when things inevitably get interesting in production.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-16
Rork Max SwiftUI App Crash Logs Are Unreadable: The dSYM Upload Pitfall
Fix Unsymbolicated crash reports in Firebase Crashlytics for Rork Max SwiftUI apps. Learn the correct dSYM upload configuration with real examples from indie app development.
Dev Tools2026-05-16
Migrating Firebase CocoaPods to Swift Package Manager in Rork Max iOS Apps — Real Migration Log and 3 Pitfalls to Avoid Before October 2026
Firebase Apple SDK's CocoaPods distribution ends in October 2026. Here's a detailed migration log from moving 4 Rork Max iOS apps to Swift Package Manager — including dSYM failures, module errors, and the Dropbox conflict copy problem you'll definitely hit.
Dev Tools2026-05-09
Building a Rork A/B Testing Platform with GrowthBook and PostHog from Scratch
Drop Firebase and pair GrowthBook with PostHog to ship a production-grade A/B testing stack for your Rork apps. SDK wiring, edge evaluation on Cloudflare Workers, Bayesian statistics interpretation, and the operational pitfalls I learned the hard way.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →