// lib/featureFlags.ts
interface FeatureFlag {
name: string;
enabled: boolean;
rolloutPercentage: number;
allowedUserIds: string[];
}
const flags: Record<string, FeatureFlag>
= {
'ai-enhance': {
name: 'AI Image Enhancement',
enabled: true,
rolloutPercentage: 10,
allowedUserIds: [
'admin-1', 'beta-tester-1'
],
},
'new-export-dialog': {
name: 'Redesigned Export',
enabled: false,
rolloutPercentage: 0,
allowedUserIds: [],
},
};
// lib/featureFlags.ts (continued)
function isFeatureEnabled(
flagName: string,
userId: string
): boolean {
const flag = flags[flagName];
if (!flag || !flag.enabled)
return false;
// Always-on for specific users
if (flag.allowedUserIds
.includes(userId))
return true;
// Percentage rollout:
// Hash the userId to get a
// consistent 0-100 number.
// Same user always gets same result.
const hash = simpleHash(
`${flagName}:${userId}`);
const bucket = hash % 100;
return bucket < flag.rolloutPercentage;
}
function simpleHash(str: string) {
let hash = 0;
for (const char of str) {
hash = ((hash << 5) - hash)
+ char.charCodeAt(0);
hash |= 0; // Convert to 32-bit int
}
return Math.abs(hash);
}
// Same user, same flag → always
// same result. No randomness.
// Deterministic by user ID.
// hooks/useFeatureFlag.ts
import { useAuth } from './useAuth';
export function useFeatureFlag(
flagName: string
): boolean {
const { user } = useAuth();
if (!user) return false;
return isFeatureEnabled(
flagName, user.id);
}
// In a component:
function Toolbar() {
const showAiEnhance =
useFeatureFlag('ai-enhance');
return (
<nav>
<button>Brightness</button>
<button>Contrast</button>
{showAiEnhance && (
<button className="new-badge">
✨ AI Enhance
</button>
)}
</nav>
);
}
// 10% of users see the button.
// 90% see the same toolbar as before.
// No separate codepaths. No branches.
// API: toggle flag
// POST /api/admin/flags/:name
app.post('/api/admin/flags/:name',
requireAdmin,
async (req, res) => {
const { name } = req.params;
const { enabled, rolloutPercentage }
= req.body;
await db.collection('featureFlags')
.updateOne(
{ name },
{ $set: {
enabled,
rolloutPercentage,
updatedAt: new Date(),
updatedBy: req.user.id,
}},
);
// Log for audit trail
logger.info('Flag updated', {
flag: name,
enabled,
rolloutPercentage,
by: req.user.email,
});
res.json({ success: true });
});
// Kill switch: one click →
// enabled: false →
// feature disappears for everyone.
// No deploy. No CI. Instant.
// Rollout plan for "AI Enhance":
// Day 1: Internal team only
// allowedUserIds: ['team-1', ...]
// rolloutPercentage: 0
// Day 2: 1% of users
// rolloutPercentage: 1
// Monitor: error rate, latency
// Day 4: 10% of users
// rolloutPercentage: 10
// Monitor: conversion, engagement
// Day 7: 50% of users
// rolloutPercentage: 50
// A/B test: compare metrics
// Day 10: 100% (full release)
// rolloutPercentage: 100
// Remove flag from code (cleanup)
git switch -c feature/PIXELCRAFT-092-feature-flags
git add src/lib/ src/hooks/ src/api/
git commit -m "Add feature flag system + AI Enhance rollout (PIXELCRAFT-092)"
git push origin feature/PIXELCRAFT-092-feature-flags
# PR → Review → Merge → Close ticket ✅
Deployment ≠ Release.
Feature flags decouple deployment from release. Code ships to production but users don't see it until you decide. This changes everything about how teams ship software.