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-configDesigning the Feature Flags Architecture
The Three-Layer Approach
A well-designed Feature Flags system consists of three layers:
- Remote Config Provider — Fetches config from Firebase and distributes it app-wide
- Feature Flag Hook — A custom hook for components to read flag values
- 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 rendersImplementing 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 onboardingDesigning 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 screenBuilding 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.