Flag Configuration
```typescript
// 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;
}
interface FeatureFlagsConfig {
flags: Record;
defaultEnabled: boolean;
}
interface UserContext {
id: string;
segments?: string[];
attributes?: Record;
}
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 && user) {
if (flag.allowedUsers.includes(user.id)) {
return true;
}
}
// Check segment allowlist
if (flag.allowedSegments?.length && user?.segments) {
const hasSegment = flag.allowedSegments.some(
segment => user.segments?.includes(segment)
);
if (hasSegment) {
return true;
}
}
// Check percentage rollout
if (flag.percentage !== undefined && user) {
return this.isInPercentage(user.id, flagName, flag.percentage);
}
// If no specific rules, use enabled status
return flag.enabled && !flag.percentage && !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 {
return { ...this.config.flags };
}
// For client-side: get all flags evaluated for a user
evaluateAll(user?: UserContext): Record {
const result: Record = {};
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
```typescript
// 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 {
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 {
return this.flags.evaluateAll(user);
}
async setFlag(flagName: string, config: FlagConfig): Promise {
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 {
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 {
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
```typescript
// feature-flag-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { DynamicFeatureFlags, UserContext } from './flag-loader';
declare global {
namespace Express {
interface Request {
featureFlags: Record;
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
```typescript
// 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 });
});
```