feature-flags

Control feature availability without deployments.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "feature-flags" with this command: npx skills add dadbodgeoff/drift/dadbodgeoff-drift-feature-flags

Feature Flags

Control feature availability without deployments.

When to Use This Skill

  • Gradual feature rollouts (1% → 10% → 100%)

  • A/B testing different implementations

  • Kill switches for problematic features

  • Beta features for specific users

  • Environment-specific features

Flag Types

Type Use Case Example

Boolean Simple on/off new_checkout_enabled

Percentage Gradual rollout new_ui: 25%

User List Beta testers beta_users: [id1, id2]

Segment User groups premium_feature: tier=pro

TypeScript Implementation

Flag Configuration

// feature-flags.ts interface FlagConfig { enabled: boolean; percentage?: number; // 0-100 for gradual rollout allowedUsers?: string[]; // Specific user IDs allowedSegments?: string[]; // User segments (e.g., 'beta', 'pro') metadata?: Record<string, unknown>; }

interface FeatureFlagsConfig { flags: Record<string, FlagConfig>; defaultEnabled: boolean; }

interface UserContext { id: string; segments?: string[]; attributes?: Record<string, unknown>; }

class FeatureFlags { private config: FeatureFlagsConfig;

constructor(config: FeatureFlagsConfig) { this.config = config; }

isEnabled(flagName: string, user?: UserContext): boolean { const flag = this.config.flags[flagName];

if (!flag) {
  return this.config.defaultEnabled;
}

if (!flag.enabled) {
  return false;
}

// Check user allowlist
if (flag.allowedUsers?.length &#x26;&#x26; user) {
  if (flag.allowedUsers.includes(user.id)) {
    return true;
  }
}

// Check segment allowlist
if (flag.allowedSegments?.length &#x26;&#x26; user?.segments) {
  const hasSegment = flag.allowedSegments.some(
    segment => user.segments?.includes(segment)
  );
  if (hasSegment) {
    return true;
  }
}

// Check percentage rollout
if (flag.percentage !== undefined &#x26;&#x26; user) {
  return this.isInPercentage(user.id, flagName, flag.percentage);
}

// If no specific rules, use enabled status
return flag.enabled &#x26;&#x26; !flag.percentage &#x26;&#x26; !flag.allowedUsers?.length;

}

private isInPercentage(userId: string, flagName: string, percentage: number): boolean { // Consistent hashing - same user always gets same result const hash = this.hashString(${userId}:${flagName}); const bucket = hash % 100; return bucket < percentage; }

private hashString(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); }

getFlag(flagName: string): FlagConfig | undefined { return this.config.flags[flagName]; }

getAllFlags(): Record<string, FlagConfig> { return { ...this.config.flags }; }

// For client-side: get all flags evaluated for a user evaluateAll(user?: UserContext): Record<string, boolean> { const result: Record<string, boolean> = {}; for (const flagName of Object.keys(this.config.flags)) { result[flagName] = this.isEnabled(flagName, user); } return result; } }

export { FeatureFlags, FlagConfig, FeatureFlagsConfig, UserContext };

Dynamic Flag Loading

// flag-loader.ts import { Redis } from 'ioredis'; import { FeatureFlags, FeatureFlagsConfig } from './feature-flags';

class DynamicFeatureFlags { private flags: FeatureFlags; private redis: Redis; private refreshInterval: NodeJS.Timeout;

constructor(redis: Redis, refreshMs = 30000) { this.redis = redis; this.flags = new FeatureFlags({ flags: {}, defaultEnabled: false });

// Refresh flags periodically
this.refreshInterval = setInterval(() => this.refresh(), refreshMs);
this.refresh();

}

async refresh(): Promise<void> { try { const configJson = await this.redis.get('feature_flags'); if (configJson) { const config: FeatureFlagsConfig = JSON.parse(configJson); this.flags = new FeatureFlags(config); } } catch (error) { console.error('Failed to refresh feature flags:', error); } }

isEnabled(flagName: string, user?: UserContext): boolean { return this.flags.isEnabled(flagName, user); }

evaluateAll(user?: UserContext): Record<string, boolean> { return this.flags.evaluateAll(user); }

async setFlag(flagName: string, config: FlagConfig): Promise<void> { const currentConfig = await this.getConfig(); currentConfig.flags[flagName] = config; await this.redis.set('feature_flags', JSON.stringify(currentConfig)); await this.refresh(); }

async deleteFlag(flagName: string): Promise<void> { const currentConfig = await this.getConfig(); delete currentConfig.flags[flagName]; await this.redis.set('feature_flags', JSON.stringify(currentConfig)); await this.refresh(); }

private async getConfig(): Promise<FeatureFlagsConfig> { const configJson = await this.redis.get('feature_flags'); return configJson ? JSON.parse(configJson) : { flags: {}, defaultEnabled: false }; }

close(): void { clearInterval(this.refreshInterval); } }

export { DynamicFeatureFlags };

Express Middleware

// feature-flag-middleware.ts import { Request, Response, NextFunction } from 'express'; import { DynamicFeatureFlags, UserContext } from './flag-loader';

declare global { namespace Express { interface Request { featureFlags: Record<string, boolean>; isFeatureEnabled: (flag: string) => boolean; } } }

function featureFlagMiddleware(flags: DynamicFeatureFlags) { return (req: Request, res: Response, next: NextFunction) => { // Build user context from request const user: UserContext | undefined = req.user ? { id: req.user.id, segments: [req.user.tier, ...(req.user.tags || [])], attributes: { email: req.user.email, createdAt: req.user.createdAt, }, } : undefined;

// Evaluate all flags for this user
req.featureFlags = flags.evaluateAll(user);

// Helper function
req.isFeatureEnabled = (flag: string) => req.featureFlags[flag] ?? false;

next();

}; }

export { featureFlagMiddleware };

Usage in Routes

// routes.ts app.get('/checkout', (req, res) => { if (req.isFeatureEnabled('new_checkout')) { return res.render('checkout-v2'); } return res.render('checkout'); });

app.get('/api/features', (req, res) => { // Send evaluated flags to frontend res.json({ flags: req.featureFlags }); });

Python Implementation

feature_flags.py

import hashlib from typing import Dict, List, Optional, Any from dataclasses import dataclass, field import json import redis

@dataclass class FlagConfig: enabled: bool percentage: Optional[int] = None allowed_users: List[str] = field(default_factory=list) allowed_segments: List[str] = field(default_factory=list) metadata: Dict[str, Any] = field(default_factory=dict)

@dataclass class UserContext: id: str segments: List[str] = field(default_factory=list) attributes: Dict[str, Any] = field(default_factory=dict)

class FeatureFlags: def init(self, flags: Dict[str, FlagConfig], default_enabled: bool = False): self.flags = flags self.default_enabled = default_enabled

def is_enabled(self, flag_name: str, user: Optional[UserContext] = None) -> bool:
    flag = self.flags.get(flag_name)
    
    if not flag:
        return self.default_enabled
    
    if not flag.enabled:
        return False

    # Check user allowlist
    if flag.allowed_users and user:
        if user.id in flag.allowed_users:
            return True

    # Check segment allowlist
    if flag.allowed_segments and user and user.segments:
        if any(seg in flag.allowed_segments for seg in user.segments):
            return True

    # Check percentage rollout
    if flag.percentage is not None and user:
        return self._is_in_percentage(user.id, flag_name, flag.percentage)

    # Default to enabled if no specific rules
    return flag.enabled and not flag.percentage and not flag.allowed_users

def _is_in_percentage(self, user_id: str, flag_name: str, percentage: int) -> bool:
    hash_input = f"{user_id}:{flag_name}".encode()
    hash_value = int(hashlib.md5(hash_input).hexdigest(), 16)
    bucket = hash_value % 100
    return bucket &#x3C; percentage

def evaluate_all(self, user: Optional[UserContext] = None) -> Dict[str, bool]:
    return {
        name: self.is_enabled(name, user)
        for name in self.flags
    }

class DynamicFeatureFlags: def init(self, redis_client: redis.Redis, key: str = "feature_flags"): self.redis = redis_client self.key = key self._flags: Optional[FeatureFlags] = None self.refresh()

def refresh(self) -> None:
    try:
        data = self.redis.get(self.key)
        if data:
            config = json.loads(data)
            flags = {
                name: FlagConfig(**flag_config)
                for name, flag_config in config.get("flags", {}).items()
            }
            self._flags = FeatureFlags(
                flags=flags,
                default_enabled=config.get("default_enabled", False)
            )
    except Exception as e:
        print(f"Failed to refresh flags: {e}")

def is_enabled(self, flag_name: str, user: Optional[UserContext] = None) -> bool:
    if not self._flags:
        return False
    return self._flags.is_enabled(flag_name, user)

def evaluate_all(self, user: Optional[UserContext] = None) -> Dict[str, bool]:
    if not self._flags:
        return {}
    return self._flags.evaluate_all(user)

def set_flag(self, flag_name: str, config: FlagConfig) -> None:
    data = self.redis.get(self.key)
    current = json.loads(data) if data else {"flags": {}, "default_enabled": False}
    current["flags"][flag_name] = {
        "enabled": config.enabled,
        "percentage": config.percentage,
        "allowed_users": config.allowed_users,
        "allowed_segments": config.allowed_segments,
        "metadata": config.metadata,
    }
    self.redis.set(self.key, json.dumps(current))
    self.refresh()

FastAPI Integration

fastapi_flags.py

from fastapi import Request, Depends from functools import lru_cache

@lru_cache() def get_feature_flags() -> DynamicFeatureFlags: return DynamicFeatureFlags(redis_client)

def get_user_context(request: Request) -> Optional[UserContext]: user = getattr(request.state, "user", None) if not user: return None return UserContext( id=user.id, segments=[user.tier] + (user.tags or []), attributes={"email": user.email}, )

@app.get("/checkout") async def checkout( flags: DynamicFeatureFlags = Depends(get_feature_flags), user: Optional[UserContext] = Depends(get_user_context), ): if flags.is_enabled("new_checkout", user): return {"template": "checkout-v2"} return {"template": "checkout"}

@app.get("/api/features") async def get_features( flags: DynamicFeatureFlags = Depends(get_feature_flags), user: Optional[UserContext] = Depends(get_user_context), ): return {"flags": flags.evaluate_all(user)}

React Integration

// FeatureFlagProvider.tsx import { createContext, useContext, useEffect, useState } from 'react';

interface FeatureFlagContextValue { flags: Record<string, boolean>; isEnabled: (flag: string) => boolean; loading: boolean; }

const FeatureFlagContext = createContext<FeatureFlagContextValue>({ flags: {}, isEnabled: () => false, loading: true, });

export function FeatureFlagProvider({ children }: { children: React.ReactNode }) { const [flags, setFlags] = useState<Record<string, boolean>>({}); const [loading, setLoading] = useState(true);

useEffect(() => { fetch('/api/features') .then(res => res.json()) .then(data => { setFlags(data.flags); setLoading(false); }); }, []);

const isEnabled = (flag: string) => flags[flag] ?? false;

return ( <FeatureFlagContext.Provider value={{ flags, isEnabled, loading }}> {children} </FeatureFlagContext.Provider> ); }

export function useFeatureFlag(flag: string): boolean { const { isEnabled } = useContext(FeatureFlagContext); return isEnabled(flag); }

// Usage function CheckoutButton() { const newCheckout = useFeatureFlag('new_checkout');

if (newCheckout) { return <NewCheckoutButton />; } return <LegacyCheckoutButton />; }

Admin API

// admin-routes.ts router.get('/admin/flags', async (req, res) => { const flags = await featureFlags.getAllFlags(); res.json({ flags }); });

router.put('/admin/flags/:name', async (req, res) => { const { name } = req.params; const config: FlagConfig = req.body;

await featureFlags.setFlag(name, config); res.json({ success: true }); });

router.delete('/admin/flags/:name', async (req, res) => { const { name } = req.params; await featureFlags.deleteFlag(name); res.json({ success: true }); });

// Gradual rollout helper router.post('/admin/flags/:name/rollout', async (req, res) => { const { name } = req.params; const { percentage } = req.body;

const current = await featureFlags.getFlag(name); if (!current) { return res.status(404).json({ error: 'Flag not found' }); }

await featureFlags.setFlag(name, { ...current, percentage, });

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

Best Practices

  • Use consistent hashing: Same user always gets same result

  • Default to disabled: New flags should be off by default

  • Clean up old flags: Remove flags after full rollout

  • Log flag evaluations: Track which users see which features

  • Cache flag config: Don't hit Redis on every request

Common Mistakes

  • Random percentage (user sees different result each request)

  • Not cleaning up old flags (config bloat)

  • Hardcoding flag names (use constants)

  • Not testing both paths

  • Forgetting to remove flag checks after rollout

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

oauth-social-login

No summary provided by upstream source.

Repository SourceNeeds Review
General

sse-streaming

No summary provided by upstream source.

Repository SourceNeeds Review
General

multi-tenancy

No summary provided by upstream source.

Repository SourceNeeds Review