Turning a feature on or off for everyone is easy. The real power of feature flags comes when you can target specific users, teams, or environments. This is where target lists transform feature flags from simple toggles into sophisticated deployment and experimentation tools.
What is User Targeting?
User targeting means controlling feature visibility based on user attributes. Instead of "everyone sees this" or "no one sees this," you get granular control:
- Internal team members see new features first
- Beta users get early access to test features
- Enterprise customers get premium features
- Users in specific regions see localized features
- Mobile users get mobile-optimized experiences
- Free tier users see upgrade prompts
This level of control changes everything about how you deploy and test software.

Why Targeting Matters
1. Safe Testing in Production
The most powerful test environment isn't staging - it's production. But you need to limit blast radius:
// Enable new checkout flow only for internal team
const targetList = {
feature: 'new-checkout-v2',
enabled: true,
targeting: {
userEmails: [
'@company.com', // All company emails
'beta-tester@example.com',
],
},
}
Your team tests with real production data, real payment processors, real third-party APIs - but customers never see the feature until it's ready.
2. Gradual Rollouts with Precision
Percentage-based rollouts are great, but sometimes you need more control:
Scenario: Launch a new recommendation algorithm
// Phase 1: Internal team (1 week)
targeting: { userIds: ['team-member-ids'] }
// Phase 2: Power users (1 week)
targeting: {
userSegments: ['power-users'],
minAccountAge: 90 // days
}
// Phase 3: Geographic rollout (2 weeks)
targeting: {
regions: ['US-West', 'US-East', 'EU', 'APAC']
// Roll out region by region
}
// Phase 4: Everyone
targeting: { enabled: true }
Each phase validates the feature with a specific group before expanding. If problems arise, they're contained.
3. Environment-Specific Features
Different environments need different features:
const featureConfig = {
'debug-panel': {
development: true,
staging: true,
production: false, // Never in prod
},
analytics: {
development: false, // Don't pollute analytics
staging: true,
production: true,
},
'payment-sandbox': {
development: true,
staging: true,
production: false, // Real payments only in prod
},
}
This prevents development-only features from reaching production and ensures production-only features (like real payment processing) stay there.
The Anatomy of a Target List
A robust targeting system needs these capabilities:
User Identifiers
- User IDs: Target specific users
- Email patterns:
*@company.comfor internal teams - User segments: Beta users, premium customers, trial users
User Attributes
- Account age: New vs returning customers
- Subscription tier: Free, Pro, Enterprise
- Feature usage: Active vs dormant users
- Geographic location: Country, region, timezone
Device & Platform
- Platform: Web, iOS, Android
- Browser: Chrome, Safari, Firefox
- Device type: Mobile, tablet, desktop
- OS version: iOS 15+, Android 12+
Business Logic
- Companies: B2B features for specific customers
- Account value: High-value vs standard customers
- Support tier: Premium support customers
- Contract terms: Early access for specific agreements
Real-World Targeting Patterns
Pattern 1: The Beta Program
const betaFeatureConfig = {
targeting: {
// Beta opt-ins
userSegments: ['beta-opted-in'],
// OR internal team members
emailPatterns: ['*@company.com'],
// OR specific VIP users
userIds: ['vip-customer-1', 'vip-customer-2'],
},
}
This gives you three ways to access: explicit opt-in, being on the team, or being a VIP customer.
Pattern 2: The Premium Feature
const premiumFeatureConfig = {
targeting: {
subscriptionTiers: ['pro', 'enterprise'],
// Grace period for recently downgraded users
customRule: (user) => {
if (user.tier === 'free' && user.recentlyDowngraded) {
return user.downgradedDate > Date.now() - 7 * 24 * 60 * 60 * 1000
}
return false
},
},
}
Pro and Enterprise users always get access. Recently downgraded users get a 7-day grace period - good for retention.
Pattern 3: The Regional Rollout
const regionalFeatureConfig = {
targeting: {
// Stage 1: Australia (small, English-speaking market)
stage: 1,
countries: ['AU', 'NZ'],
// Stage 2: Expand to US/UK after 1 week
// stage: 2,
// countries: ['AU', 'NZ', 'US', 'GB'],
// Stage 3: Europe
// stage: 3,
// countries: ['AU', 'NZ', 'US', 'GB', 'DE', 'FR', 'ES', 'IT'],
// Stage 4: Everywhere
// stage: 4,
// enabled: true
},
}
Test in a small English-speaking market first, then expand systematically.
Pattern 4: The Mobile-First Feature
const mobileFeatureConfig = {
targeting: {
platforms: ['ios', 'android'],
// Minimum versions that support the feature
minVersions: {
ios: '15.0',
android: '12.0',
},
// Gradual rollout on mobile
percentage: 50, // 50% of mobile users
},
}
Launch on mobile where the feature works best, validate, then build desktop version.
Implementing Target Lists
Basic Implementation
interface TargetingRule {
userIds?: string[]
emailPatterns?: string[]
userSegments?: string[]
countries?: string[]
subscriptionTiers?: string[]
percentage?: number
customRule?: (user: User) => boolean
}
async function evaluateTargeting(user: User, rules: TargetingRule): Promise<boolean> {
// Check user IDs
if (rules.userIds && rules.userIds.includes(user.id)) {
return true
}
// Check email patterns
if (rules.emailPatterns) {
for (const pattern of rules.emailPatterns) {
if (matchesPattern(user.email, pattern)) {
return true
}
}
}
// Check user segments
if (rules.userSegments) {
const userSegments = await getUserSegments(user.id)
if (userSegments.some((s) => rules.userSegments!.includes(s))) {
return true
}
}
// Check geographic location
if (rules.countries && rules.countries.includes(user.country)) {
return true
}
// Check subscription tier
if (rules.subscriptionTiers && rules.subscriptionTiers.includes(user.tier)) {
return true
}
// Percentage-based rollout
if (rules.percentage) {
const hash = hashUser(user.id)
if (hash % 100 < rules.percentage) {
return true
}
}
// Custom rules
if (rules.customRule) {
return rules.customRule(user)
}
return false
}
Advanced: Multi-Environment Targeting
interface EnvironmentConfig {
development: TargetingRule
staging: TargetingRule
production: TargetingRule
}
const multiEnvFeature: EnvironmentConfig = {
development: {
// Everyone in dev
enabled: true,
},
staging: {
// Team + beta users in staging
emailPatterns: ['*@company.com'],
userSegments: ['beta-testers'],
},
production: {
// Gradual rollout in production
percentage: 10,
countries: ['US'], // US only for now
subscriptionTiers: ['pro', 'enterprise'], // Premium only
},
}
Common Pitfalls and Solutions
Pitfall 1: Targeting Rules Too Complex
Problem: Rules become unreadable and hard to debug
// Bad: Nested complexity
if (
(user.tier === 'pro' || user.tier === 'enterprise') &&
(user.country === 'US' || user.country === 'CA') &&
(user.accountAge > 30 || user.revenueTotal > 1000) &&
!user.hasOptedOut &&
user.emailVerified
) {
// When does this run? Hard to tell!
}
Solution: Use named segments
// Good: Named, testable segments
const segments = {
'premium-customers': (u) => ['pro', 'enterprise'].includes(u.tier),
'north-america': (u) => ['US', 'CA'].includes(u.country),
'established-users': (u) => u.accountAge > 30 || u.revenueTotal > 1000,
'eligible-users': (u) => !u.hasOptedOut && u.emailVerified,
}
if (
isInSegments(user, ['premium-customers', 'north-america', 'established-users', 'eligible-users'])
) {
// Clear and testable
}
Pitfall 2: Inconsistent User Assignment
Problem: Same user gets different features on different devices
// Bad: Random percentage each time
if (Math.random() < 0.5) {
return true
}
Solution: Hash-based consistent assignment
// Good: Same user always gets same result
function isInPercentage(userId: string, percentage: number): boolean {
const hash = hashFunction(userId)
return hash % 100 < percentage
}
Pitfall 3: Target Lists That Never Get Cleaned Up
Problem: "Temporary" target lists become permanent
// Bad: Forever in the codebase
if (userIds.includes('old-beta-tester-from-2019')) {
return true
}
Solution: Time-bound targeting with automatic expiration
// Good: Expires automatically
const betaConfig = {
targeting: {
userSegments: ['beta-testers'],
expiresAt: '2024-12-31', // Automatic cleanup
},
}
Best Practices
1. Start Narrow, Expand Gradually
Begin with the smallest possible group:
- Your team (10 people)
- Beta users (100 people)
- 1% of users (1,000 people)
- 10% of users (10,000 people)
- Everyone (100,000+ people)
Each step validates the feature before the next expansion.
2. Document Targeting Intent
const targetingConfig = {
feature: 'new-dashboard',
intent: 'Phase 2: Beta users in North America',
reasoning: 'Validating with English-speaking users before localization',
nextStep: 'Expand to EU after translation',
owner: 'product-team@company.com',
targeting: {
userSegments: ['beta-opted-in'],
countries: ['US', 'CA'],
},
}
Future you will appreciate knowing why these targeting rules exist.
3. Monitor Segment Health
Track metrics per segment:
- Error rates: Is one segment experiencing more errors?
- Performance: Does the feature perform differently per segment?
- Engagement: Are targeted users actually using the feature?
- Conversion: Does the feature improve outcomes for the segment?
4. Provide Override Mechanisms
// Allow support team to enable features for specific users
if (adminOverride.enabled && adminOverride.userId === user.id) {
return true
}
// Allow users to opt-out
if (userPreferences.optedOut.includes(featureId)) {
return false
}
Sometimes you need to override targeting logic. Build escape hatches.
Targeting + Other Flag Patterns
Targeting becomes even more powerful when combined with other patterns:
Targeting + Kill Switches
const featureConfig = {
killSwitch: false, // Override everything if true
targeting: {
// Normal targeting rules
},
}
if (featureConfig.killSwitch) {
return false // Feature disabled for everyone
}
Targeting + Percentage Rollouts
const featureConfig = {
targeting: {
userSegments: ['premium-customers'], // Only premium customers
percentage: 25, // 25% of premium customers
},
}
Targeting + A/B Tests
const abTestConfig = {
targeting: {
userSegments: ['web-users'], // Only web users in test
},
variants: {
control: 50,
treatment: 50,
},
}
The Future: AI-Powered Targeting
Advanced systems are moving toward predictive targeting:
const aiTargeting = {
objective: 'maximize-conversion',
learnFrom: ['past-feature-adoptions', 'user-behavior-patterns'],
autoTarget: {
mostLikely: 'to-adopt',
leastLikely: 'to-churn',
highestValue: 'potential-revenue',
},
}
Machine learning identifies which users are most likely to benefit from new features and targets them automatically.
Conclusion
Feature flags without targeting are on/off switches. Feature flags with targeting are precision instruments for:
- Safe production testing with limited blast radius
- Gradual rollouts that validate before scaling
- Environment-specific behavior
- Premium features for premium customers
- Regional customization
- Platform-specific optimizations
The difference between "should we launch this?" and "who should see this first?" is the difference between hoping for the best and engineering for success.
Start simple - target your team, then beta users, then segments. As you master targeting, you'll wonder how you ever deployed features any other way.
Your users aren't a monolith. Your feature rollouts shouldn't be either.
