Feature Flag Systems for ChatGPT Apps: Gradual Rollouts & Emergency Kill Switches

Feature flags transform ChatGPT app development from binary releases into graduated, data-driven deployments. Instead of shipping new inline card layouts to 800 million users at once and praying for success, you deploy to 5%, monitor error rates and conversion metrics, then scale to 25%, 50%, and 100% over days—minimizing blast radius from bugs while maximizing learning velocity. When experiments fail catastrophically, kill switches revert changes instantly without redeploying code.

For ChatGPT apps targeting a massive user base, feature flags are non-negotiable infrastructure. A single bad deployment can generate thousands of 1-star reviews before you notice. Gradual rollouts catch critical issues at 5% exposure (affecting 40,000 users) instead of 100% (800 million users). This guide delivers production-ready systems for custom feature flag services, Redis flag stores, client-side evaluation, user targeting, percentage rollouts, multi-variate flags, and lifecycle management—empowering you to ship safely at ChatGPT scale.

Feature Flags vs Feature Toggles: Architecture Fundamentals

Feature flags and feature toggles are often used interchangeably, but subtle architectural distinctions matter at scale. Feature flags control availability of entire features ("enable new PiP widget"), while feature toggles control behavior within features ("sort PiP items by recency vs. relevance"). Both decouple deployment from release: code ships to production in "dark mode," activated only when flags flip.

Boolean Flags: Simple on/off switches for binary decisions (e.g., new_inline_card_enabled: true). Use for feature readiness gates: code deploys to production with flag=false, QA validates in staging, then flip to true for production traffic. No redeploy required.

Percentage Rollouts: Gradually increase exposure from 0% → 5% → 25% → 50% → 100% over hours or days. Deterministic hashing ensures users consistently see the same variant across sessions (hash(userId + flagKey) % 100 < rolloutPercentage). Rollouts detect performance regressions before they affect all users.

User Targeting: Complex rules like "show variant B to mobile users in US with free tier accounts created after January 1, 2026." Enables segment-specific optimization without code duplication. Targeting prevents accidental exposure to unready segments (e.g., don't show premium features to free users).

Multi-Variate Flags: Return configuration objects instead of booleans (getVariant('inline_card_layout'){ maxActions: 2, showImages: true }). Supports A/B/C/D testing and complex experiments without hardcoding variants.

Here's the decision framework:

Use Case Flag Type Example
New feature ready for production Boolean Flag fullscreen_map_view: true
Gradual deployment to mitigate risk Percentage Rollout new_checkout_flow: 25%
Segment-specific features User Targeting premium_analytics: tier='professional'
A/B testing with variants Multi-Variate Flag cta_button: {text: 'Start Trial', color: '#D4AF37'}
Emergency rollback Kill Switch disable_payment_gateway: true

Real-World Example: A fitness studio ChatGPT app wants to test a new class booking flow. Instead of deploying to all users:

  1. Week 1: Deploy code with new_booking_flow: false (dark mode)
  2. Week 2: Test internally with targeting: { email: 'contains @makeaihq.com' }
  3. Week 3: Gradual rollout: 5% → 25% → 50% (monitor error rates, conversion)
  4. Week 4: If metrics improve, 100%; if metrics degrade, revert to 0% (kill switch)

This approach catches a critical bug at 5% exposure (affecting 5,000 daily users) instead of 100% (100,000 daily users), preventing catastrophic UX failures and 1-star review floods.

Custom Feature Flag Service: Core Implementation

Building a custom feature flag service gives you full control over evaluation logic, targeting rules, and real-time updates without vendor lock-in. The service must support client-side evaluation for performance (no network latency on every flag check), server-side evaluation for security-sensitive flags, and real-time updates via WebSockets or polling.

Architecture Requirements:

  • Flag Storage: Centralized store (Redis, Firestore) with millisecond read latency
  • Evaluation Engine: Client-side JavaScript for UX flags, server-side Node.js for authorization flags
  • Cache Layer: localStorage caching with 30-minute TTL to minimize API calls
  • Update Mechanism: WebSocket push for instant flag changes, polling fallback every 5 minutes
  • Audit Log: Track all flag changes with timestamps, user IDs, and rollback capability

Performance Constraints: Flags must not block rendering. Evaluate flags synchronously from cache, fetch updates asynchronously. Never make synchronous API calls in render paths. Target <50ms for flag evaluation across 100+ flags.

Here's a production-ready feature flag service (TypeScript, 160+ lines):

// feature-flag-service.ts - Client-side feature flag evaluation with caching
import crypto from 'crypto';

interface FlagConfig {
  key: string;
  enabled: boolean;
  rolloutPercentage: number; // 0-100
  targeting?: TargetingRule[];
  variants?: Record<string, any>;
  defaultVariant?: string;
  lastUpdated: Date;
}

interface TargetingRule {
  attribute: string; // 'userId', 'email', 'tier', 'country', 'platform', 'createdAt'
  operator: 'equals' | 'contains' | 'in' | 'greaterThan' | 'lessThan' | 'regex';
  value: any;
  negate?: boolean; // NOT operator
}

interface UserContext {
  userId: string;
  email?: string;
  tier?: 'free' | 'starter' | 'professional' | 'business';
  country?: string;
  platform?: 'mobile' | 'desktop' | 'tablet';
  createdAt?: Date;
  customAttributes?: Record<string, any>;
}

interface CacheEntry {
  value: any;
  timestamp: number;
}

class FeatureFlagService {
  private flags: Map<string, FlagConfig> = new Map();
  private cache: Map<string, CacheEntry> = new Map();
  private readonly CACHE_TTL = 30 * 60 * 1000; // 30 minutes
  private readonly REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
  private websocket?: WebSocket;

  constructor(
    private context: UserContext,
    private apiEndpoint: string = '/api/feature-flags',
    private enableWebSocket: boolean = true
  ) {
    this.initialize();
  }

  private async initialize(): Promise<void> {
    // Load flags from API
    await this.loadFlagsFromAPI();

    // Setup WebSocket for real-time updates
    if (this.enableWebSocket) {
      this.setupWebSocket();
    }

    // Fallback polling (if WebSocket fails or disabled)
    setInterval(() => this.refresh(), this.REFRESH_INTERVAL);

    console.log(`✅ FeatureFlagService initialized (${this.flags.size} flags loaded)`);
  }

  private async loadFlagsFromAPI(): Promise<void> {
    try {
      const response = await fetch(this.apiEndpoint, {
        headers: {
          'Authorization': `Bearer ${this.getAuthToken()}`,
          'Content-Type': 'application/json'
        }
      });

      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }

      const flags: FlagConfig[] = await response.json();

      flags.forEach(flag => {
        this.flags.set(flag.key, {
          ...flag,
          lastUpdated: new Date(flag.lastUpdated)
        });
      });

      console.log(`✅ Loaded ${flags.length} feature flags from API`);
    } catch (error) {
      console.error('❌ Failed to load feature flags:', error);
      // Use cached flags as fallback
      this.loadFromLocalStorage();
    }
  }

  private setupWebSocket(): void {
    try {
      const wsUrl = this.apiEndpoint.replace(/^http/, 'ws') + '/live';
      this.websocket = new WebSocket(wsUrl);

      this.websocket.onmessage = (event) => {
        try {
          const update = JSON.parse(event.data);

          if (update.type === 'flag_updated') {
            const flag = update.flag as FlagConfig;
            this.flags.set(flag.key, flag);
            this.cache.delete(flag.key); // Invalidate cache

            console.log(`🔄 Flag updated via WebSocket: ${flag.key}`);
          }
        } catch (error) {
          console.error('WebSocket message error:', error);
        }
      };

      this.websocket.onerror = () => {
        console.warn('⚠️ WebSocket error, falling back to polling');
      };

      this.websocket.onclose = () => {
        console.log('WebSocket closed, using polling for updates');
      };
    } catch (error) {
      console.warn('⚠️ WebSocket setup failed, using polling only', error);
    }
  }

  // Evaluate feature flag for current user
  isEnabled(key: string): boolean {
    const cached = this.getCached(key);
    if (cached !== undefined) return cached;

    const flag = this.flags.get(key);
    if (!flag) {
      console.warn(`⚠️ Flag not found: ${key}, defaulting to false`);
      return false;
    }

    // Check if flag is globally disabled
    if (!flag.enabled) {
      this.setCached(key, false);
      return false;
    }

    // Check targeting rules (if ANY rule matches, flag is enabled for user)
    if (flag.targeting && flag.targeting.length > 0) {
      if (!this.matchesTargeting(flag.targeting)) {
        this.setCached(key, false);
        return false;
      }
    }

    // Check rollout percentage
    const isInRollout = this.isInRollout(key, flag.rolloutPercentage);
    this.setCached(key, isInRollout);

    return isInRollout;
  }

  // Get variant value (for A/B tests and multi-variate flags)
  getVariant<T = any>(key: string): T | undefined {
    const flag = this.flags.get(key);
    if (!flag || !flag.variants) return undefined;

    if (!this.isEnabled(key)) {
      return flag.defaultVariant
        ? flag.variants[flag.defaultVariant]
        : undefined;
    }

    // For A/B tests: deterministically assign to variant
    const variantKeys = Object.keys(flag.variants);
    if (variantKeys.length === 0) return undefined;

    const hash = this.hashUserId(this.context.userId, key);
    const index = hash % variantKeys.length;
    const variantKey = variantKeys[index];

    return flag.variants[variantKey];
  }

  // Get all enabled flags (useful for debugging)
  getEnabledFlags(): string[] {
    return Array.from(this.flags.keys()).filter(key => this.isEnabled(key));
  }

  private matchesTargeting(rules: TargetingRule[]): boolean {
    // ALL rules must match (AND logic)
    return rules.every(rule => {
      const value = this.context.customAttributes?.[rule.attribute] ??
                    (this.context as any)[rule.attribute];

      if (value === undefined) return false;

      let matches = false;

      switch (rule.operator) {
        case 'equals':
          matches = value === rule.value;
          break;
        case 'contains':
          matches = String(value).includes(String(rule.value));
          break;
        case 'in':
          matches = Array.isArray(rule.value) && rule.value.includes(value);
          break;
        case 'greaterThan':
          matches = value > rule.value;
          break;
        case 'lessThan':
          matches = value < rule.value;
          break;
        case 'regex':
          matches = new RegExp(rule.value).test(String(value));
          break;
        default:
          return false;
      }

      return rule.negate ? !matches : matches;
    });
  }

  private isInRollout(key: string, percentage: number): boolean {
    if (percentage >= 100) return true;
    if (percentage <= 0) return false;

    // Deterministic hashing for consistent assignment
    const hash = this.hashUserId(this.context.userId, key);
    const hashValue = hash / 0xffffffff; // Normalize to 0-1

    return hashValue < (percentage / 100);
  }

  private hashUserId(userId: string, key: string): number {
    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${key}`)
      .digest('hex');

    return parseInt(hash.substring(0, 8), 16);
  }

  private getCached(key: string): boolean | undefined {
    const cached = this.cache.get(key);
    if (!cached) return undefined;

    const age = Date.now() - cached.timestamp;
    if (age > this.CACHE_TTL) {
      this.cache.delete(key);
      return undefined;
    }

    return cached.value;
  }

  private setCached(key: string, value: boolean): void {
    this.cache.set(key, { value, timestamp: Date.now() });
  }

  private getAuthToken(): string {
    // In production, retrieve from auth store/localStorage
    return localStorage.getItem('authToken') || '';
  }

  private loadFromLocalStorage(): void {
    try {
      const stored = localStorage.getItem('feature_flags');
      if (stored) {
        const flags: FlagConfig[] = JSON.parse(stored);
        flags.forEach(flag => this.flags.set(flag.key, flag));
        console.log(`✅ Loaded ${flags.length} flags from localStorage (fallback)`);
      }
    } catch (error) {
      console.error('Failed to load from localStorage:', error);
    }
  }

  private saveToLocalStorage(): void {
    try {
      const flags = Array.from(this.flags.values());
      localStorage.setItem('feature_flags', JSON.stringify(flags));
    } catch (error) {
      console.error('Failed to save to localStorage:', error);
    }
  }

  // Refresh flags (call on session start or every 5 minutes)
  async refresh(): Promise<void> {
    this.cache.clear();
    await this.loadFlagsFromAPI();
    this.saveToLocalStorage();
  }

  // Cleanup on unmount
  destroy(): void {
    if (this.websocket) {
      this.websocket.close();
    }
  }
}

export default FeatureFlagService;

Usage Example:

// In your ChatGPT widget initialization
const userContext: UserContext = {
  userId: 'user_12345',
  email: 'customer@example.com',
  tier: 'professional',
  country: 'US',
  platform: 'mobile',
  createdAt: new Date('2026-01-15')
};

const flags = new FeatureFlagService(userContext, 'https://api.makeaihq.com/feature-flags');

// Check if feature is enabled
if (flags.isEnabled('new_inline_card_layout')) {
  renderNewInlineCard();
} else {
  renderLegacyInlineCard();
}

// Get A/B test variant
const ctaConfig = flags.getVariant<{ text: string; color: string }>('cta_button_test');
if (ctaConfig) {
  renderCTA(ctaConfig.text, ctaConfig.color);
}

Redis Flag Store: High-Performance Backend

Redis provides sub-millisecond flag lookups at ChatGPT scale (800M users). Instead of querying a database for every flag evaluation, cache all flags in Redis with 5-minute TTL. Flags update via pub/sub: when admin changes a flag, Redis broadcasts to all app instances, invalidating caches instantly.

Redis Data Structures:

  • Hash: flags:{flagKey} stores flag configuration (enabled, rollout, targeting)
  • Set: flags:enabled stores all currently enabled flag keys (fast membership checks)
  • Pub/Sub: flags:updates channel broadcasts flag changes to all subscribers

Performance: Redis can serve 100K+ flag evaluations/second from a single node. For ChatGPT apps with millions of concurrent users, this scales horizontally with Redis Cluster.

Here's a production Redis flag store (TypeScript, 140+ lines):

// redis-flag-store.ts - Server-side flag persistence with Redis
import Redis from 'ioredis';

interface FlagConfig {
  key: string;
  enabled: boolean;
  rolloutPercentage: number;
  targeting?: TargetingRule[];
  variants?: Record<string, any>;
  defaultVariant?: string;
  createdAt: Date;
  updatedAt: Date;
}

interface TargetingRule {
  attribute: string;
  operator: string;
  value: any;
  negate?: boolean;
}

class RedisFlagStore {
  private redis: Redis;
  private subscriber: Redis;
  private readonly FLAG_PREFIX = 'flags:';
  private readonly ENABLED_SET = 'flags:enabled';
  private readonly UPDATE_CHANNEL = 'flags:updates';

  constructor(redisUrl: string = 'redis://localhost:6379') {
    this.redis = new Redis(redisUrl);
    this.subscriber = new Redis(redisUrl);
    this.setupSubscriber();
  }

  private setupSubscriber(): void {
    this.subscriber.subscribe(this.UPDATE_CHANNEL);

    this.subscriber.on('message', (channel, message) => {
      if (channel === this.UPDATE_CHANNEL) {
        console.log(`🔄 Flag update received: ${message}`);
        // Trigger client cache invalidation via WebSocket broadcast
        this.broadcastUpdate(JSON.parse(message));
      }
    });
  }

  // Create or update flag
  async setFlag(config: FlagConfig): Promise<void> {
    const key = `${this.FLAG_PREFIX}${config.key}`;

    const flagData = {
      ...config,
      updatedAt: new Date()
    };

    // Store flag config as JSON
    await this.redis.set(key, JSON.stringify(flagData));

    // Update enabled set
    if (config.enabled) {
      await this.redis.sadd(this.ENABLED_SET, config.key);
    } else {
      await this.redis.srem(this.ENABLED_SET, config.key);
    }

    // Publish update to all subscribers
    await this.redis.publish(this.UPDATE_CHANNEL, JSON.stringify({
      type: 'flag_updated',
      flag: flagData
    }));

    console.log(`✅ Flag saved: ${config.key} (enabled: ${config.enabled}, rollout: ${config.rolloutPercentage}%)`);
  }

  // Get single flag
  async getFlag(key: string): Promise<FlagConfig | null> {
    const flagKey = `${this.FLAG_PREFIX}${key}`;
    const data = await this.redis.get(flagKey);

    if (!data) return null;

    return JSON.parse(data);
  }

  // Get all flags
  async getAllFlags(): Promise<FlagConfig[]> {
    const keys = await this.redis.keys(`${this.FLAG_PREFIX}*`);

    if (keys.length === 0) return [];

    const pipeline = this.redis.pipeline();
    keys.forEach(key => pipeline.get(key));

    const results = await pipeline.exec();

    return results
      ?.map(([err, data]) => (err ? null : JSON.parse(data as string)))
      .filter((flag): flag is FlagConfig => flag !== null) || [];
  }

  // Get enabled flags only
  async getEnabledFlags(): Promise<string[]> {
    return await this.redis.smembers(this.ENABLED_SET);
  }

  // Delete flag
  async deleteFlag(key: string): Promise<void> {
    const flagKey = `${this.FLAG_PREFIX}${key}`;

    await this.redis.del(flagKey);
    await this.redis.srem(this.ENABLED_SET, key);

    await this.redis.publish(this.UPDATE_CHANNEL, JSON.stringify({
      type: 'flag_deleted',
      key
    }));

    console.log(`🗑️ Flag deleted: ${key}`);
  }

  // Batch update flags (for admin dashboard)
  async batchUpdateFlags(updates: Partial<FlagConfig>[]): Promise<void> {
    const pipeline = this.redis.pipeline();

    for (const update of updates) {
      if (!update.key) continue;

      const existingFlag = await this.getFlag(update.key);
      if (!existingFlag) continue;

      const updatedFlag: FlagConfig = {
        ...existingFlag,
        ...update,
        updatedAt: new Date()
      };

      const key = `${this.FLAG_PREFIX}${update.key}`;
      pipeline.set(key, JSON.stringify(updatedFlag));

      if (updatedFlag.enabled) {
        pipeline.sadd(this.ENABLED_SET, updatedFlag.key);
      } else {
        pipeline.srem(this.ENABLED_SET, updatedFlag.key);
      }
    }

    await pipeline.exec();

    console.log(`✅ Batch updated ${updates.length} flags`);
  }

  // Get flags for specific user (with targeting evaluation)
  async getFlagsForUser(userContext: any): Promise<Record<string, boolean>> {
    const flags = await this.getAllFlags();
    const result: Record<string, boolean> = {};

    for (const flag of flags) {
      if (!flag.enabled) {
        result[flag.key] = false;
        continue;
      }

      // Check targeting
      if (flag.targeting && flag.targeting.length > 0) {
        const matches = this.evaluateTargeting(flag.targeting, userContext);
        if (!matches) {
          result[flag.key] = false;
          continue;
        }
      }

      // Check rollout percentage
      const inRollout = this.isInRollout(userContext.userId, flag.key, flag.rolloutPercentage);
      result[flag.key] = inRollout;
    }

    return result;
  }

  private evaluateTargeting(rules: TargetingRule[], context: any): boolean {
    return rules.every(rule => {
      const value = context[rule.attribute];
      if (value === undefined) return false;

      let matches = false;

      switch (rule.operator) {
        case 'equals':
          matches = value === rule.value;
          break;
        case 'contains':
          matches = String(value).includes(String(rule.value));
          break;
        case 'in':
          matches = Array.isArray(rule.value) && rule.value.includes(value);
          break;
        case 'greaterThan':
          matches = value > rule.value;
          break;
        case 'lessThan':
          matches = value < rule.value;
          break;
        default:
          return false;
      }

      return rule.negate ? !matches : matches;
    });
  }

  private isInRollout(userId: string, flagKey: string, percentage: number): boolean {
    if (percentage >= 100) return true;
    if (percentage <= 0) return false;

    const crypto = require('crypto');
    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${flagKey}`)
      .digest('hex');

    const hashValue = parseInt(hash.substring(0, 8), 16) / 0xffffffff;

    return hashValue < (percentage / 100);
  }

  private broadcastUpdate(update: any): void {
    // In production, this would send WebSocket message to all connected clients
    // For now, just log
    console.log('Broadcasting update to clients:', update);
  }

  // Cleanup
  async disconnect(): Promise<void> {
    await this.redis.quit();
    await this.subscriber.quit();
  }
}

export default RedisFlagStore;

Usage Example:

// Server-side API endpoint
import express from 'express';
import RedisFlagStore from './redis-flag-store';

const app = express();
const flagStore = new RedisFlagStore('redis://localhost:6379');

// Admin endpoint: Create/update flag
app.post('/api/admin/flags', async (req, res) => {
  const { key, enabled, rolloutPercentage, targeting, variants } = req.body;

  await flagStore.setFlag({
    key,
    enabled,
    rolloutPercentage,
    targeting,
    variants,
    createdAt: new Date(),
    updatedAt: new Date()
  });

  res.json({ success: true });
});

// Client endpoint: Get all flags for user
app.get('/api/feature-flags', async (req, res) => {
  const userId = req.user.id; // From auth middleware
  const userContext = {
    userId,
    email: req.user.email,
    tier: req.user.tier,
    country: req.user.country
  };

  const flags = await flagStore.getFlagsForUser(userContext);
  res.json(flags);
});

app.listen(3000);

Evaluation Engine: Client vs Server-Side Logic

Feature flag evaluation can happen client-side (JavaScript in browser) or server-side (Node.js API). Each approach has tradeoffs:

Client-Side Evaluation:

  • Pros: Zero latency (no network calls), works offline, reduces server load
  • Cons: Exposes flag logic to users (inspect network tab), can't hide features completely, limited to non-sensitive flags
  • Use Cases: UX changes (inline card layouts, button colors), A/B tests, performance optimizations

Server-Side Evaluation:

  • Pros: Flag logic hidden from users, secure for authorization/billing flags, centralized control
  • Cons: Network latency on every check, higher server load, requires API infrastructure
  • Use Cases: Feature access control (premium features), billing/payment gates, security-sensitive flags

Hybrid Approach: Most production systems use both. Evaluate UX flags client-side for performance, authorization flags server-side for security. Cache server-side results in session tokens to minimize latency.

Here's a production evaluation engine (TypeScript, 130+ lines):

// evaluation-engine.ts - Client + server evaluation with caching
interface EvaluationResult {
  enabled: boolean;
  variant?: any;
  reason: string; // For debugging
}

class EvaluationEngine {
  // Client-side evaluation (fast, no network)
  evaluateClient(
    flag: FlagConfig,
    userContext: UserContext
  ): EvaluationResult {
    // Global kill switch
    if (!flag.enabled) {
      return {
        enabled: false,
        reason: 'Flag globally disabled'
      };
    }

    // Targeting rules
    if (flag.targeting && !this.matchesTargeting(flag.targeting, userContext)) {
      return {
        enabled: false,
        reason: 'User does not match targeting rules'
      };
    }

    // Rollout percentage
    if (!this.isInRollout(userContext.userId, flag.key, flag.rolloutPercentage)) {
      return {
        enabled: false,
        reason: `User outside ${flag.rolloutPercentage}% rollout`
      };
    }

    // Get variant (if multi-variate flag)
    const variant = flag.variants
      ? this.selectVariant(flag, userContext.userId)
      : undefined;

    return {
      enabled: true,
      variant,
      reason: 'All checks passed'
    };
  }

  // Server-side evaluation (secure, can check database/billing)
  async evaluateServer(
    flagKey: string,
    userContext: UserContext,
    additionalChecks?: {
      checkSubscription?: boolean;
      checkQuota?: boolean;
    }
  ): Promise<EvaluationResult> {
    // Fetch flag from Redis/database
    const flag = await this.fetchFlag(flagKey);

    if (!flag) {
      return {
        enabled: false,
        reason: 'Flag not found'
      };
    }

    // Run client-side checks first
    const clientResult = this.evaluateClient(flag, userContext);
    if (!clientResult.enabled) {
      return clientResult;
    }

    // Additional server-side checks
    if (additionalChecks?.checkSubscription) {
      const hasAccess = await this.checkSubscriptionAccess(userContext.userId, flagKey);
      if (!hasAccess) {
        return {
          enabled: false,
          reason: 'Subscription tier insufficient'
        };
      }
    }

    if (additionalChecks?.checkQuota) {
      const withinQuota = await this.checkQuota(userContext.userId, flagKey);
      if (!withinQuota) {
        return {
          enabled: false,
          reason: 'Usage quota exceeded'
        };
      }
    }

    return {
      enabled: true,
      variant: clientResult.variant,
      reason: 'All checks passed (including server-side)'
    };
  }

  private matchesTargeting(rules: TargetingRule[], context: UserContext): boolean {
    return rules.every(rule => {
      const value = context.customAttributes?.[rule.attribute] ??
                    (context as any)[rule.attribute];

      if (value === undefined) return false;

      let matches = false;

      switch (rule.operator) {
        case 'equals':
          matches = value === rule.value;
          break;
        case 'contains':
          matches = String(value).includes(String(rule.value));
          break;
        case 'in':
          matches = Array.isArray(rule.value) && rule.value.includes(value);
          break;
        case 'greaterThan':
          matches = value > rule.value;
          break;
        case 'lessThan':
          matches = value < rule.value;
          break;
        case 'regex':
          matches = new RegExp(rule.value).test(String(value));
          break;
        default:
          return false;
      }

      return rule.negate ? !matches : matches;
    });
  }

  private isInRollout(userId: string, flagKey: string, percentage: number): boolean {
    if (percentage >= 100) return true;
    if (percentage <= 0) return false;

    const crypto = require('crypto');
    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${flagKey}`)
      .digest('hex');

    const hashValue = parseInt(hash.substring(0, 8), 16) / 0xffffffff;

    return hashValue < (percentage / 100);
  }

  private selectVariant(flag: FlagConfig, userId: string): any {
    if (!flag.variants) return undefined;

    const variantKeys = Object.keys(flag.variants);
    if (variantKeys.length === 0) return undefined;

    const crypto = require('crypto');
    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${flag.key}:variant`)
      .digest('hex');

    const index = parseInt(hash.substring(0, 8), 16) % variantKeys.length;
    const variantKey = variantKeys[index];

    return flag.variants[variantKey];
  }

  private async fetchFlag(key: string): Promise<FlagConfig | null> {
    // In production, fetch from Redis/database
    // For now, mock implementation
    return null;
  }

  private async checkSubscriptionAccess(userId: string, flagKey: string): Promise<boolean> {
    // Query billing system to check if user's subscription tier allows this feature
    // Example: SELECT tier FROM subscriptions WHERE userId = ?
    return true; // Mock
  }

  private async checkQuota(userId: string, flagKey: string): Promise<boolean> {
    // Check if user has exceeded usage quota for this feature
    // Example: SELECT COUNT(*) FROM usage WHERE userId = ? AND feature = ?
    return true; // Mock
  }
}

export default EvaluationEngine;

Client Integration: React Feature Flag Hook

React hooks provide ergonomic client-side flag evaluation. Instead of manually calling featureFlagService.isEnabled() everywhere, use a useFeatureFlag hook that handles caching, re-evaluation on context changes, and automatic cleanup.

Performance: Hook should memoize results to prevent re-evaluation on every render. Only re-evaluate when user context changes or flags are updated via WebSocket.

Here's a production React hook (TypeScript, 140+ lines):

// use-feature-flag.ts - React hook for feature flags
import { useState, useEffect, useMemo, useCallback } from 'react';
import FeatureFlagService from './feature-flag-service';

interface UseFeatureFlagOptions {
  defaultValue?: boolean;
  onEnabled?: () => void;
  onDisabled?: () => void;
}

// Singleton service instance (shared across all hook calls)
let globalFlagService: FeatureFlagService | null = null;

// Initialize service once per app lifecycle
export function initializeFeatureFlags(
  userContext: UserContext,
  apiEndpoint?: string
): void {
  if (!globalFlagService) {
    globalFlagService = new FeatureFlagService(userContext, apiEndpoint);
  }
}

// Hook: Check if feature is enabled
export function useFeatureFlag(
  flagKey: string,
  options: UseFeatureFlagOptions = {}
): boolean {
  if (!globalFlagService) {
    console.error('❌ Feature flags not initialized. Call initializeFeatureFlags() first.');
    return options.defaultValue ?? false;
  }

  const [enabled, setEnabled] = useState<boolean>(
    globalFlagService.isEnabled(flagKey)
  );

  useEffect(() => {
    // Re-evaluate flag (handles cache invalidation from WebSocket updates)
    const checkFlag = () => {
      const newValue = globalFlagService!.isEnabled(flagKey);

      if (newValue !== enabled) {
        setEnabled(newValue);

        // Trigger callbacks
        if (newValue && options.onEnabled) {
          options.onEnabled();
        } else if (!newValue && options.onDisabled) {
          options.onDisabled();
        }
      }
    };

    // Check every 30 seconds (in case WebSocket fails)
    const interval = setInterval(checkFlag, 30000);

    return () => clearInterval(interval);
  }, [flagKey, enabled, options]);

  return enabled;
}

// Hook: Get variant value
export function useFeatureFlagVariant<T = any>(
  flagKey: string,
  defaultVariant?: T
): T | undefined {
  if (!globalFlagService) {
    console.error('❌ Feature flags not initialized.');
    return defaultVariant;
  }

  const [variant, setVariant] = useState<T | undefined>(
    globalFlagService.getVariant<T>(flagKey)
  );

  useEffect(() => {
    const checkVariant = () => {
      const newVariant = globalFlagService!.getVariant<T>(flagKey);
      setVariant(newVariant);
    };

    const interval = setInterval(checkVariant, 30000);
    return () => clearInterval(interval);
  }, [flagKey]);

  return variant ?? defaultVariant;
}

// Hook: Get all enabled flags (for debugging)
export function useEnabledFlags(): string[] {
  if (!globalFlagService) return [];

  const [enabledFlags, setEnabledFlags] = useState<string[]>(
    globalFlagService.getEnabledFlags()
  );

  useEffect(() => {
    const checkFlags = () => {
      setEnabledFlags(globalFlagService!.getEnabledFlags());
    };

    const interval = setInterval(checkFlags, 30000);
    return () => clearInterval(interval);
  }, []);

  return enabledFlags;
}

// Component: Conditional rendering based on flag
interface FeatureFlagProps {
  flagKey: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

export const FeatureFlag: React.FC<FeatureFlagProps> = ({
  flagKey,
  children,
  fallback = null
}) => {
  const enabled = useFeatureFlag(flagKey);

  return <>{enabled ? children : fallback}</>;
};

// Example usage in component
export function ExampleComponent() {
  // Simple boolean flag
  const newLayoutEnabled = useFeatureFlag('new_inline_card_layout');

  // Variant flag with callback
  const ctaConfig = useFeatureFlagVariant<{ text: string; color: string }>(
    'cta_button_test',
    { text: 'Learn More', color: '#666' } // Default
  );

  // Debug: All enabled flags
  const enabledFlags = useEnabledFlags();

  return (
    <div>
      {/* Conditional rendering with hook */}
      {newLayoutEnabled ? (
        <NewInlineCard />
      ) : (
        <LegacyInlineCard />
      )}

      {/* Conditional rendering with component */}
      <FeatureFlag flagKey="premium_analytics" fallback={<UpgradeCTA />}>
        <AnalyticsDashboard />
      </FeatureFlag>

      {/* Variant-based rendering */}
      <button style={{ backgroundColor: ctaConfig.color }}>
        {ctaConfig.text}
      </button>

      {/* Debug panel */}
      <details>
        <summary>Enabled Flags ({enabledFlags.length})</summary>
        <ul>
          {enabledFlags.map(flag => (
            <li key={flag}>{flag}</li>
          ))}
        </ul>
      </details>
    </div>
  );
}

App Initialization:

// App.tsx
import { initializeFeatureFlags } from './use-feature-flag';
import { useAuth } from './auth-context';

function App() {
  const { user } = useAuth();

  useEffect(() => {
    if (user) {
      initializeFeatureFlags({
        userId: user.id,
        email: user.email,
        tier: user.tier,
        country: user.country,
        platform: /mobile/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
        createdAt: new Date(user.createdAt)
      }, 'https://api.makeaihq.com/feature-flags');
    }
  }, [user]);

  return <Router>...</Router>;
}

Server-Side Flag Evaluation: Express Middleware

For authorization and billing flags, evaluate server-side to prevent client-side tampering. Express middleware can inject flag evaluation into request pipeline, making flags available via req.flags.

Security: Never trust client-side flag evaluation for premium features or billing gates. Always validate server-side with checkSubscription and checkQuota guards.

Here's a production Express middleware (TypeScript, 130+ lines):

// flag-middleware.ts - Express middleware for server-side flag evaluation
import { Request, Response, NextFunction } from 'express';
import RedisFlagStore from './redis-flag-store';
import EvaluationEngine from './evaluation-engine';

interface FlagMiddlewareOptions {
  redisUrl?: string;
  cacheEnabled?: boolean;
  cacheTTL?: number; // milliseconds
}

// Extend Express Request type
declare global {
  namespace Express {
    interface Request {
      flags: {
        isEnabled: (key: string) => Promise<boolean>;
        getVariant: <T>(key: string) => Promise<T | undefined>;
        evaluateWithChecks: (
          key: string,
          options?: { checkSubscription?: boolean; checkQuota?: boolean }
        ) => Promise<boolean>;
      };
    }
  }
}

class FlagMiddleware {
  private flagStore: RedisFlagStore;
  private evaluationEngine: EvaluationEngine;
  private cache: Map<string, { value: boolean; timestamp: number }>;
  private cacheTTL: number;

  constructor(options: FlagMiddlewareOptions = {}) {
    this.flagStore = new RedisFlagStore(options.redisUrl);
    this.evaluationEngine = new EvaluationEngine();
    this.cache = new Map();
    this.cacheTTL = options.cacheTTL ?? 5 * 60 * 1000; // 5 minutes default
  }

  // Middleware function
  middleware() {
    return async (req: Request, res: Response, next: NextFunction) => {
      // Get user context from auth middleware
      const userContext = this.extractUserContext(req);

      // Attach flag evaluation methods to request
      req.flags = {
        isEnabled: async (key: string) => {
          return this.evaluateFlag(key, userContext);
        },

        getVariant: async <T>(key: string): Promise<T | undefined> => {
          return this.evaluateFlagVariant<T>(key, userContext);
        },

        evaluateWithChecks: async (key: string, options = {}) => {
          const result = await this.evaluationEngine.evaluateServer(
            key,
            userContext,
            options
          );
          return result.enabled;
        }
      };

      next();
    };
  }

  private extractUserContext(req: Request): UserContext {
    // Extract from JWT token or session
    const user = (req as any).user; // Assuming auth middleware sets req.user

    return {
      userId: user?.id || 'anonymous',
      email: user?.email,
      tier: user?.tier,
      country: req.headers['cf-ipcountry'] as string || 'US', // Cloudflare header
      platform: /mobile/i.test(req.headers['user-agent'] || '') ? 'mobile' : 'desktop',
      createdAt: user?.createdAt ? new Date(user.createdAt) : undefined
    };
  }

  private async evaluateFlag(
    key: string,
    userContext: UserContext
  ): Promise<boolean> {
    // Check cache first
    const cached = this.getCached(key, userContext.userId);
    if (cached !== undefined) return cached;

    // Fetch flag from Redis
    const flag = await this.flagStore.getFlag(key);
    if (!flag) return false;

    // Evaluate
    const result = this.evaluationEngine.evaluateClient(flag, userContext);

    // Cache result
    this.setCached(key, userContext.userId, result.enabled);

    return result.enabled;
  }

  private async evaluateFlagVariant<T>(
    key: string,
    userContext: UserContext
  ): Promise<T | undefined> {
    const flag = await this.flagStore.getFlag(key);
    if (!flag || !flag.variants) return undefined;

    const result = this.evaluationEngine.evaluateClient(flag, userContext);

    return result.variant as T;
  }

  private getCached(key: string, userId: string): boolean | undefined {
    const cacheKey = `${key}:${userId}`;
    const cached = this.cache.get(cacheKey);

    if (!cached) return undefined;

    const age = Date.now() - cached.timestamp;
    if (age > this.cacheTTL) {
      this.cache.delete(cacheKey);
      return undefined;
    }

    return cached.value;
  }

  private setCached(key: string, userId: string, value: boolean): void {
    const cacheKey = `${key}:${userId}`;
    this.cache.set(cacheKey, { value, timestamp: Date.now() });
  }
}

export default FlagMiddleware;

Usage in Express App:

// server.ts
import express from 'express';
import FlagMiddleware from './flag-middleware';

const app = express();

// Initialize flag middleware
const flagMiddleware = new FlagMiddleware({
  redisUrl: 'redis://localhost:6379',
  cacheEnabled: true,
  cacheTTL: 5 * 60 * 1000 // 5 minutes
});

// Apply globally (after auth middleware)
app.use(flagMiddleware.middleware());

// Route: Check premium feature access
app.get('/api/analytics', async (req, res) => {
  // Evaluate with subscription check
  const hasAccess = await req.flags.evaluateWithChecks('premium_analytics', {
    checkSubscription: true,
    checkQuota: false
  });

  if (!hasAccess) {
    return res.status(403).json({ error: 'Upgrade to Professional tier for analytics' });
  }

  // User has access, return analytics data
  const analytics = await getAnalyticsData(req.user.id);
  res.json(analytics);
});

// Route: A/B test variant
app.get('/api/homepage', async (req, res) => {
  const heroConfig = await req.flags.getVariant<{ headline: string; cta: string }>('homepage_hero');

  res.json({
    headline: heroConfig?.headline || 'Build ChatGPT Apps',
    cta: heroConfig?.cta || 'Start Free Trial'
  });
});

Flag Analytics Tracker: Monitoring & Insights

Track flag exposure and conversion events to measure experiment performance. Analytics reveal which flags drive engagement, which cause errors, and which are no longer used (technical debt).

Key Metrics:

  • Exposure Rate: % of users who see the flag
  • Conversion Rate: % of exposed users who complete goal action
  • Error Rate: % of exposed users who encounter errors
  • Adoption Curve: Daily active users over time (for gradual rollouts)

Here's a production analytics tracker (TypeScript, 100+ lines):

// flag-analytics-tracker.ts - Track flag exposure and conversions
interface FlagEvent {
  type: 'exposure' | 'conversion' | 'error';
  flagKey: string;
  userId: string;
  variant?: string;
  timestamp: Date;
  metadata?: Record<string, any>;
}

class FlagAnalyticsTracker {
  private events: FlagEvent[] = [];

  // Track flag exposure
  trackExposure(
    flagKey: string,
    userId: string,
    variant?: string,
    metadata?: Record<string, any>
  ): void {
    const event: FlagEvent = {
      type: 'exposure',
      flagKey,
      userId,
      variant,
      timestamp: new Date(),
      metadata
    };

    this.events.push(event);
    this.sendToAnalytics(event);

    console.log(`👁️ Flag exposure: ${flagKey} (user: ${userId}, variant: ${variant})`);
  }

  // Track conversion
  trackConversion(
    flagKey: string,
    userId: string,
    variant?: string,
    metadata?: Record<string, any>
  ): void {
    const event: FlagEvent = {
      type: 'conversion',
      flagKey,
      userId,
      variant,
      timestamp: new Date(),
      metadata
    };

    this.events.push(event);
    this.sendToAnalytics(event);

    console.log(`✅ Flag conversion: ${flagKey} (user: ${userId}, variant: ${variant})`);
  }

  // Track error
  trackError(
    flagKey: string,
    userId: string,
    error: Error,
    metadata?: Record<string, any>
  ): void {
    const event: FlagEvent = {
      type: 'error',
      flagKey,
      userId,
      timestamp: new Date(),
      metadata: {
        ...metadata,
        error: error.message,
        stack: error.stack
      }
    };

    this.events.push(event);
    this.sendToAnalytics(event);

    console.error(`❌ Flag error: ${flagKey} (user: ${userId})`, error);
  }

  private sendToAnalytics(event: FlagEvent): void {
    // Google Analytics 4
    if (typeof gtag !== 'undefined') {
      gtag('event', `flag_${event.type}`, {
        flag_key: event.flagKey,
        user_id: event.userId,
        variant: event.variant,
        ...event.metadata
      });
    }

    // Mixpanel
    if (typeof mixpanel !== 'undefined') {
      mixpanel.track(`Flag ${event.type}`, {
        flagKey: event.flagKey,
        userId: event.userId,
        variant: event.variant,
        ...event.metadata
      });
    }

    // Custom analytics endpoint
    fetch('/api/analytics/flag-events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)
    }).catch(err => console.error('Analytics error:', err));
  }

  // Generate report for flag
  generateReport(flagKey: string): {
    exposures: number;
    conversions: number;
    conversionRate: number;
    errors: number;
    errorRate: number;
  } {
    const flagEvents = this.events.filter(e => e.flagKey === flagKey);

    const exposures = flagEvents.filter(e => e.type === 'exposure').length;
    const conversions = flagEvents.filter(e => e.type === 'conversion').length;
    const errors = flagEvents.filter(e => e.type === 'error').length;

    return {
      exposures,
      conversions,
      conversionRate: exposures > 0 ? conversions / exposures : 0,
      errors,
      errorRate: exposures > 0 ? errors / exposures : 0
    };
  }
}

export default FlagAnalyticsTracker;

Integration with Feature Flag Service:

// Modified FeatureFlagService to include analytics
class FeatureFlagServiceWithAnalytics extends FeatureFlagService {
  private analytics = new FlagAnalyticsTracker();

  isEnabled(key: string): boolean {
    const enabled = super.isEnabled(key);

    // Track exposure
    this.analytics.trackExposure(key, this.context.userId);

    return enabled;
  }

  getVariant<T>(key: string): T | undefined {
    const variant = super.getVariant<T>(key);

    // Track exposure with variant
    this.analytics.trackExposure(key, this.context.userId, JSON.stringify(variant));

    return variant;
  }

  // Method for tracking conversions
  trackConversion(key: string, metadata?: Record<string, any>): void {
    this.analytics.trackConversion(key, this.context.userId, undefined, metadata);
  }
}

Advanced User Targeting: Segmentation Engine

User targeting enables segment-specific optimization. Instead of one-size-fits-all rollouts, deploy features to "mobile users in US with professional tier" first, validate product-market fit, then expand to other segments.

Targeting Operators:

  • equals: Exact match (tier === 'professional')
  • contains: Substring match (email.contains('@makeaihq.com'))
  • in: Array membership (country in ['US', 'CA', 'UK'])
  • greaterThan/lessThan: Numeric comparison (createdAt > 2026-01-01)
  • regex: Pattern matching (email.matches(/.*@enterprise\.com$/i))

Complex Rules: Combine multiple conditions with AND logic (tier=professional AND country=US AND platform=mobile).

Here's a production segmentation engine (TypeScript, 130+ lines):

// user-segmentation-engine.ts - Advanced targeting with complex rules
interface SegmentRule {
  attribute: string;
  operator: 'equals' | 'contains' | 'in' | 'greaterThan' | 'lessThan' | 'regex' | 'between';
  value: any;
  negate?: boolean; // NOT operator
}

interface Segment {
  id: string;
  name: string;
  description: string;
  rules: SegmentRule[];
  priority: number; // Higher priority wins in conflicts
}

class UserSegmentationEngine {
  private segments: Map<string, Segment> = new Map();

  // Define segment
  defineSegment(segment: Segment): void {
    this.segments.set(segment.id, segment);
    console.log(`✅ Segment defined: ${segment.name} (${segment.rules.length} rules)`);
  }

  // Check if user matches segment
  matchesSegment(segmentId: string, userContext: UserContext): boolean {
    const segment = this.segments.get(segmentId);
    if (!segment) return false;

    return this.evaluateRules(segment.rules, userContext);
  }

  // Get all matching segments for user (ordered by priority)
  getMatchingSegments(userContext: UserContext): Segment[] {
    const matching = Array.from(this.segments.values())
      .filter(segment => this.evaluateRules(segment.rules, userContext))
      .sort((a, b) => b.priority - a.priority); // Descending priority

    return matching;
  }

  private evaluateRules(rules: SegmentRule[], context: UserContext): boolean {
    // ALL rules must match (AND logic)
    return rules.every(rule => {
      const value = context.customAttributes?.[rule.attribute] ??
                    (context as any)[rule.attribute];

      if (value === undefined) return rule.negate ? true : false;

      let matches = false;

      switch (rule.operator) {
        case 'equals':
          matches = value === rule.value;
          break;

        case 'contains':
          matches = String(value).toLowerCase().includes(String(rule.value).toLowerCase());
          break;

        case 'in':
          matches = Array.isArray(rule.value) && rule.value.includes(value);
          break;

        case 'greaterThan':
          matches = value > rule.value;
          break;

        case 'lessThan':
          matches = value < rule.value;
          break;

        case 'between':
          if (Array.isArray(rule.value) && rule.value.length === 2) {
            matches = value >= rule.value[0] && value <= rule.value[1];
          }
          break;

        case 'regex':
          try {
            matches = new RegExp(rule.value, 'i').test(String(value));
          } catch (e) {
            console.error(`Invalid regex: ${rule.value}`, e);
            matches = false;
          }
          break;

        default:
          matches = false;
      }

      return rule.negate ? !matches : matches;
    });
  }

  // Analyze segment overlap (for debugging targeting conflicts)
  analyzeOverlap(segmentIds: string[]): {
    overlapping: boolean;
    conflicts: string[];
  } {
    // This would analyze if segments have conflicting rules
    // For production, implement conflict detection logic
    return {
      overlapping: false,
      conflicts: []
    };
  }
}

export default UserSegmentationEngine;

Usage Example:

const segmentation = new UserSegmentationEngine();

// Define segments
segmentation.defineSegment({
  id: 'enterprise_beta_testers',
  name: 'Enterprise Beta Testers',
  description: 'Large organizations testing new features',
  priority: 100,
  rules: [
    { attribute: 'tier', operator: 'equals', value: 'business' },
    { attribute: 'createdAt', operator: 'lessThan', value: new Date('2026-01-01') },
    { attribute: 'email', operator: 'regex', value: '.*@(enterprise|corp)\\.com$' }
  ]
});

segmentation.defineSegment({
  id: 'mobile_power_users',
  name: 'Mobile Power Users',
  description: 'Heavy mobile app users',
  priority: 50,
  rules: [
    { attribute: 'platform', operator: 'equals', value: 'mobile' },
    { attribute: 'sessionCount', operator: 'greaterThan', value: 100 }
  ]
});

// Check if user matches segment
const userContext: UserContext = {
  userId: 'user_789',
  email: 'admin@enterprise.com',
  tier: 'business',
  platform: 'mobile',
  createdAt: new Date('2024-11-15'),
  customAttributes: {
    sessionCount: 150
  }
};

const isEnterpriseBeta = segmentation.matchesSegment('enterprise_beta_testers', userContext);
console.log(`Enterprise beta tester: ${isEnterpriseBeta}`); // true

const matchingSegments = segmentation.getMatchingSegments(userContext);
console.log(`Matching segments: ${matchingSegments.map(s => s.name).join(', ')}`);

Percentage Rollout: Deterministic Hashing

Percentage-based rollouts gradually increase exposure (5% → 25% → 50% → 100%) while ensuring users consistently see the same variant. Deterministic hashing guarantees hash(userId + flagKey) % 100 produces the same result across sessions, preventing users from flickering between variants.

Hashing Requirements:

  • Deterministic: Same input produces same output
  • Uniform Distribution: 0-100 range evenly distributed
  • Collision Resistant: Minimal chance of duplicate hashes
  • Fast: <1ms for millions of users

MD5 vs SHA256: MD5 is faster (sufficient for feature flags), SHA256 is more secure (use for cryptographic applications). For feature flags, MD5 is the standard.

Here's a production rollout implementation (TypeScript, 120+ lines):

// percentage-rollout.ts - Deterministic gradual rollout
import crypto from 'crypto';

interface RolloutConfig {
  flagKey: string;
  stages: RolloutStage[];
  currentStage: number;
  startDate: Date;
}

interface RolloutStage {
  percentage: number;
  duration: number; // hours
  minSuccessRate?: number; // Stop rollout if below this
}

class PercentageRollout {
  private rollouts: Map<string, RolloutConfig> = new Map();

  // Create gradual rollout plan
  createRollout(config: RolloutConfig): void {
    this.rollouts.set(config.flagKey, config);

    console.log(`✅ Rollout plan created: ${config.flagKey}`);
    console.log(`   Stages: ${config.stages.map(s => `${s.percentage}%`).join(' → ')}`);
  }

  // Check if user is in current rollout percentage
  isInRollout(userId: string, flagKey: string): boolean {
    const rollout = this.rollouts.get(flagKey);
    if (!rollout) return false;

    // Get current stage
    const stage = rollout.stages[rollout.currentStage];
    if (!stage) return false;

    return this.hashRollout(userId, flagKey, stage.percentage);
  }

  // Advance to next rollout stage
  advanceStage(flagKey: string, successRate: number): boolean {
    const rollout = this.rollouts.get(flagKey);
    if (!rollout) return false;

    const currentStage = rollout.stages[rollout.currentStage];

    // Check if success rate meets threshold
    if (currentStage.minSuccessRate && successRate < currentStage.minSuccessRate) {
      console.warn(`⚠️ Rollout halted: success rate ${successRate.toFixed(2)} < ${currentStage.minSuccessRate}`);
      return false;
    }

    // Advance to next stage
    if (rollout.currentStage < rollout.stages.length - 1) {
      rollout.currentStage++;
      const newStage = rollout.stages[rollout.currentStage];

      console.log(`🚀 Rollout advanced: ${flagKey} → ${newStage.percentage}%`);
      return true;
    } else {
      console.log(`✅ Rollout complete: ${flagKey} at 100%`);
      return false;
    }
  }

  // Rollback to previous stage (emergency)
  rollback(flagKey: string): void {
    const rollout = this.rollouts.get(flagKey);
    if (!rollout) return;

    if (rollout.currentStage > 0) {
      rollout.currentStage--;
      const stage = rollout.stages[rollout.currentStage];

      console.log(`🔙 Rollout rollback: ${flagKey} → ${stage.percentage}%`);
    } else {
      console.log(`🛑 Rollout stopped: ${flagKey} → 0%`);
    }
  }

  private hashRollout(userId: string, flagKey: string, percentage: number): boolean {
    if (percentage >= 100) return true;
    if (percentage <= 0) return false;

    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${flagKey}`)
      .digest('hex');

    // Convert first 8 hex chars to 0-100 range
    const hashValue = parseInt(hash.substring(0, 8), 16) / 0xffffffff;
    const bucket = Math.floor(hashValue * 100);

    return bucket < percentage;
  }

  // Get rollout status
  getStatus(flagKey: string): {
    currentPercentage: number;
    stage: number;
    totalStages: number;
    nextPercentage?: number;
  } | null {
    const rollout = this.rollouts.get(flagKey);
    if (!rollout) return null;

    const currentStage = rollout.stages[rollout.currentStage];
    const nextStage = rollout.stages[rollout.currentStage + 1];

    return {
      currentPercentage: currentStage.percentage,
      stage: rollout.currentStage + 1,
      totalStages: rollout.stages.length,
      nextPercentage: nextStage?.percentage
    };
  }
}

export default PercentageRollout;

Usage Example:

const rollout = new PercentageRollout();

// Define rollout plan
rollout.createRollout({
  flagKey: 'new_checkout_flow',
  currentStage: 0,
  startDate: new Date('2026-12-26'),
  stages: [
    { percentage: 5, duration: 24, minSuccessRate: 0.95 },   // 5% for 24 hours
    { percentage: 25, duration: 24, minSuccessRate: 0.93 },  // 25% for 24 hours
    { percentage: 50, duration: 48, minSuccessRate: 0.90 },  // 50% for 48 hours
    { percentage: 100, duration: 0 }                         // 100% (complete)
  ]
});

// Check if user is in rollout
const userId = 'user_456';
const inRollout = rollout.isInRollout(userId, 'new_checkout_flow');

if (inRollout) {
  console.log('User sees new checkout flow');
} else {
  console.log('User sees old checkout flow');
}

// Monitor and advance (runs every 24 hours)
setInterval(() => {
  const successRate = measureSuccessRate('new_checkout_flow');
  const advanced = rollout.advanceStage('new_checkout_flow', successRate);

  if (!advanced) {
    console.log('Rollout halted or complete');
  }
}, 24 * 60 * 60 * 1000);

// Emergency rollback
if (errorRateSpike) {
  rollout.rollback('new_checkout_flow');
}

Multi-Variate Flags: Configuration Objects

Multi-variate flags return configuration objects instead of booleans, enabling A/B/C/D testing and complex experiments without hardcoding variants in source code.

Use Cases:

  • A/B Testing: Different button colors, headlines, CTAs
  • Configuration Variants: { maxActions: 2, showImages: true } vs { maxActions: 3, showImages: false }
  • Feature Graduations: Bronze/Silver/Gold feature sets per tier

Here's a production multi-variate implementation (TypeScript, 90+ lines):

// multi-variate-flags.ts - Configuration-based feature variants
interface MultiVariateFlag<T> {
  key: string;
  enabled: boolean;
  variants: Record<string, T>;
  defaultVariant: string;
  distribution?: Record<string, number>; // Percentage allocation
}

class MultiVariateFlagService<T = any> {
  private flags: Map<string, MultiVariateFlag<T>> = new Map();

  // Define multi-variate flag
  defineFlag(flag: MultiVariateFlag<T>): void {
    // Validate distribution sums to 100
    if (flag.distribution) {
      const total = Object.values(flag.distribution).reduce((sum, pct) => sum + pct, 0);
      if (Math.abs(total - 100) > 0.01) {
        throw new Error(`Distribution must sum to 100% (got ${total}%)`);
      }
    }

    this.flags.set(flag.key, flag);
    console.log(`✅ Multi-variate flag defined: ${flag.key} (${Object.keys(flag.variants).length} variants)`);
  }

  // Get variant for user
  getVariant(userId: string, flagKey: string): T | undefined {
    const flag = this.flags.get(flagKey);
    if (!flag || !flag.enabled) {
      return flag?.variants[flag.defaultVariant];
    }

    // If distribution specified, use weighted random assignment
    if (flag.distribution) {
      const variantKey = this.selectWeightedVariant(userId, flagKey, flag.distribution);
      return flag.variants[variantKey];
    }

    // Otherwise, uniform distribution
    const variantKeys = Object.keys(flag.variants);
    const hash = this.hashUserId(userId, flagKey);
    const index = hash % variantKeys.length;

    return flag.variants[variantKeys[index]];
  }

  private selectWeightedVariant(
    userId: string,
    flagKey: string,
    distribution: Record<string, number>
  ): string {
    const hash = this.hashUserId(userId, flagKey);
    const hashValue = hash / 0xffffffff; // 0-1
    const bucket = hashValue * 100; // 0-100

    let cumulative = 0;
    for (const [variantKey, percentage] of Object.entries(distribution)) {
      cumulative += percentage;
      if (bucket < cumulative) {
        return variantKey;
      }
    }

    // Fallback (shouldn't reach here if distribution sums to 100)
    return Object.keys(distribution)[0];
  }

  private hashUserId(userId: string, flagKey: string): number {
    const crypto = require('crypto');
    const hash = crypto
      .createHash('md5')
      .update(`${userId}:${flagKey}`)
      .digest('hex');

    return parseInt(hash.substring(0, 8), 16);
  }
}

export default MultiVariateFlagService;

Usage Example:

interface CTAConfig {
  text: string;
  color: string;
  icon?: string;
}

const mvFlags = new MultiVariateFlagService<CTAConfig>();

// Define A/B/C test
mvFlags.defineFlag({
  key: 'cta_button_test',
  enabled: true,
  defaultVariant: 'control',
  variants: {
    control: { text: 'Learn More', color: '#666666' },
    treatment_a: { text: 'Start Free Trial', color: '#D4AF37', icon: 'arrow-right' },
    treatment_b: { text: 'Get Started', color: '#059669', icon: 'check' }
  },
  distribution: {
    control: 50,      // 50%
    treatment_a: 25,  // 25%
    treatment_b: 25   // 25%
  }
});

// Get variant for user
const userId = 'user_999';
const ctaConfig = mvFlags.getVariant(userId, 'cta_button_test');

console.log(`CTA Config:`, ctaConfig);
// Output: { text: 'Start Free Trial', color: '#D4AF37', icon: 'arrow-right' }

Flag Lifecycle Management: Cleanup & Technical Debt

Feature flags accumulate as technical debt. A flag deployed in January 2026 for gradual rollout should be removed by March 2026 once fully deployed. Stale flags bloat codebases, confuse developers, and slow performance (evaluating 100 unused flags on every page load).

Lifecycle Stages:

  1. Temporary: Short-lived flags for rollouts/experiments (remove after 30-90 days)
  2. Permanent: Long-term flags for premium features, kill switches (keep indefinitely)
  3. Retired: Flags no longer evaluated (safe to delete from code)

Automated Cleanup: Detect flags not evaluated in 90 days, notify developers, auto-archive in Redis.

Here's a production cleanup detector (TypeScript, 100+ lines):

// flag-cleanup-detector.ts - Detect stale flags for removal
interface FlagMetadata {
  key: string;
  createdAt: Date;
  lastEvaluated?: Date;
  evaluationCount: number;
  flagType: 'temporary' | 'permanent';
  expiresAt?: Date;
}

class FlagCleanupDetector {
  private metadata: Map<string, FlagMetadata> = new Map();
  private readonly STALE_THRESHOLD_DAYS = 90;
  private readonly EXPIRY_WARNING_DAYS = 7;

  // Register flag
  registerFlag(
    key: string,
    flagType: 'temporary' | 'permanent',
    expiresAt?: Date
  ): void {
    this.metadata.set(key, {
      key,
      createdAt: new Date(),
      evaluationCount: 0,
      flagType,
      expiresAt
    });
  }

  // Record evaluation (call on every flag check)
  recordEvaluation(key: string): void {
    const meta = this.metadata.get(key);
    if (!meta) return;

    meta.lastEvaluated = new Date();
    meta.evaluationCount++;
  }

  // Detect stale flags
  detectStaleFlags(): string[] {
    const now = Date.now();
    const staleThreshold = this.STALE_THRESHOLD_DAYS * 24 * 60 * 60 * 1000;

    return Array.from(this.metadata.values())
      .filter(meta => {
        if (meta.flagType === 'permanent') return false; // Never stale

        const lastEval = meta.lastEvaluated?.getTime() || meta.createdAt.getTime();
        const age = now - lastEval;

        return age > staleThreshold;
      })
      .map(meta => meta.key);
  }

  // Detect expiring flags
  detectExpiringFlags(): Array<{ key: string; daysUntilExpiry: number }> {
    const now = Date.now();

    return Array.from(this.metadata.values())
      .filter(meta => meta.expiresAt !== undefined)
      .map(meta => ({
        key: meta.key,
        daysUntilExpiry: Math.ceil((meta.expiresAt!.getTime() - now) / (24 * 60 * 60 * 1000))
      }))
      .filter(({ daysUntilExpiry }) =>
        daysUntilExpiry <= this.EXPIRY_WARNING_DAYS && daysUntilExpiry > 0
      );
  }

  // Generate cleanup report
  generateReport(): {
    totalFlags: number;
    temporaryFlags: number;
    permanentFlags: number;
    staleFlags: string[];
    expiringFlags: Array<{ key: string; daysUntilExpiry: number }>;
  } {
    const staleFlags = this.detectStaleFlags();
    const expiringFlags = this.detectExpiringFlags();

    const temporaryFlags = Array.from(this.metadata.values())
      .filter(m => m.flagType === 'temporary').length;

    const permanentFlags = Array.from(this.metadata.values())
      .filter(m => m.flagType === 'permanent').length;

    return {
      totalFlags: this.metadata.size,
      temporaryFlags,
      permanentFlags,
      staleFlags,
      expiringFlags
    };
  }

  // Get usage statistics
  getUsageStats(key: string): {
    evaluationCount: number;
    daysSinceCreated: number;
    daysSinceLastEvaluated: number;
  } | null {
    const meta = this.metadata.get(key);
    if (!meta) return null;

    const now = Date.now();
    const daysSinceCreated = (now - meta.createdAt.getTime()) / (24 * 60 * 60 * 1000);
    const daysSinceLastEvaluated = meta.lastEvaluated
      ? (now - meta.lastEvaluated.getTime()) / (24 * 60 * 60 * 1000)
      : daysSinceCreated;

    return {
      evaluationCount: meta.evaluationCount,
      daysSinceCreated: Math.floor(daysSinceCreated),
      daysSinceLastEvaluated: Math.floor(daysSinceLastEvaluated)
    };
  }
}

export default FlagCleanupDetector;

Usage Example:

const cleanup = new FlagCleanupDetector();

// Register flags
cleanup.registerFlag('new_checkout_flow', 'temporary', new Date('2026-03-31'));
cleanup.registerFlag('premium_analytics_gate', 'permanent');
cleanup.registerFlag('ab_test_cta_button', 'temporary', new Date('2026-02-28'));

// Record evaluations (automatically tracked in FeatureFlagService)
cleanup.recordEvaluation('new_checkout_flow');
cleanup.recordEvaluation('premium_analytics_gate');

// Generate cleanup report (run weekly via cron)
const report = cleanup.generateReport();

console.log('📊 Flag Cleanup Report:');
console.log(`   Total flags: ${report.totalFlags}`);
console.log(`   Temporary: ${report.temporaryFlags}, Permanent: ${report.permanentFlags}`);
console.log(`   Stale flags (90+ days): ${report.staleFlags.join(', ')}`);
console.log(`   Expiring soon: ${report.expiringFlags.map(f => `${f.key} (${f.daysUntilExpiry} days)`).join(', ')}`);

// Notify developers of stale flags
if (report.staleFlags.length > 0) {
  sendSlackNotification(`⚠️ ${report.staleFlags.length} stale feature flags detected. Consider removing: ${report.staleFlags.join(', ')}`);
}

Production Deployment Checklist

Before deploying feature flag infrastructure to production, validate these critical requirements:

Infrastructure

  • Redis instance running (persistent storage, not cache-only)
  • WebSocket server for real-time updates (fallback to polling)
  • API endpoints deployed (/api/feature-flags, /api/admin/flags)
  • Monitoring alerts for flag evaluation errors

Security

  • Server-side evaluation for premium/billing flags
  • Admin dashboard requires authentication (role: admin)
  • Flag updates logged with user ID and timestamp
  • No sensitive data in flag configurations (no API keys, secrets)

Performance

  • Client-side cache with 30-minute TTL
  • Flag evaluation <50ms for 100+ flags
  • Redis pipeline for batch operations
  • No synchronous API calls in render paths

Rollout Strategy

  • Gradual rollout plan (5% → 25% → 50% → 100%)
  • Error rate monitoring at each stage
  • Kill switch ready (instant rollback to 0%)
  • Success metrics defined (conversion rate, error rate)

Lifecycle Management

  • All flags tagged as temporary or permanent
  • Expiry dates set for temporary flags
  • Weekly cleanup reports (Slack notifications)
  • Stale flag removal after 90 days

Analytics Integration

  • Exposure events tracked (Google Analytics, Mixpanel)
  • Conversion events tracked
  • Error events tracked
  • Dashboard for A/B test results

Documentation

  • Flag naming conventions documented
  • Rollout process documented
  • Emergency rollback procedure documented
  • Team trained on flag creation/management

Conclusion: Ship Safely at ChatGPT Scale

Feature flag systems transform ChatGPT app development from risky big-bang releases into graduated, data-driven deployments. By implementing custom flag services, Redis persistence, client-side evaluation, user targeting, percentage rollouts, multi-variate flags, and lifecycle management, you eliminate catastrophic deployment failures while maximizing learning velocity. The frameworks presented here—from deterministic hashing to segmentation engines to cleanup detectors—provide production-ready foundations for shipping safely to 800 million ChatGPT users.

Related Resources:

External Resources:

Ready to deploy feature flags for your ChatGPT app? Start your free trial and access production-ready flag infrastructure, Redis stores, analytics dashboards, and gradual rollout automation—all optimized for ChatGPT App Store success. No credit card required.


About MakeAIHQ: We're the leading no-code platform for ChatGPT app development, empowering businesses to reach 800 million weekly ChatGPT users without writing code. From feature flags to A/B testing to canary releases, MakeAIHQ delivers enterprise-grade deployment infrastructure that ships safely at scale. Learn more about our deployment platform →