Feature flags (also known as feature toggles) are one of the most powerful tools in a modern SaaS team's arsenal. They allow you to safely roll out new features, experiment with different user groups, and quickly roll back if something goes wrong — without a new deployment.
In this article, we'll take a deep dive into feature flags: how they work, when to use them, and how to implement them in a production SaaS environment.
What are feature flags?
A feature flag is simply a condition in your code that determines whether a piece of functionality is active:
if (featureFlags.isEnabled('new-dashboard', { userId: user.id })) {
return <NewDashboard />;
} else {
return <LegacyDashboard />;
}
Instead of deploying code and immediately making it live for everyone, you wrap new functionality in a flag. You can then toggle that flag on and off via a configuration panel, without changing code or redeploying.
Why feature flags are essential for SaaS
1. Enabling trunk-based development
Without feature flags, teams often work with long-lived feature branches. This leads to merge conflicts, integration issues, and slow release cycles. With feature flags, you can push unfinished code to main — the flag keeps it hidden from users.
2. Progressive rollout
Instead of activating a feature for all your 10,000 users at once, you can scale up gradually:
- 1% — internal testing
- 5% — beta users
- 25% — early adopters
- 100% — full rollout
If problems arise at any point, you turn off the flag and it's resolved.
3. Built-in A/B testing
Feature flags are the foundation for A/B testing. You can offer two variants of a feature to different user groups and measure which performs better.
4. Plan-based features
For SaaS with multiple pricing tiers, feature flags are ideal:
const PLAN_FEATURES = {
starter: ['basic-analytics', 'email-support'],
professional: ['basic-analytics', 'email-support', 'advanced-reports', 'api-access'],
enterprise: ['basic-analytics', 'email-support', 'advanced-reports', 'api-access', 'sso', 'audit-log', 'custom-branding'],
};
Building feature flags yourself: a practical implementation
Let's build a lightweight feature flag system that you can use in your own SaaS.
Database schema
CREATE TABLE feature_flags (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT false,
rollout_percentage INT DEFAULT 0,
target_plans TEXT[] DEFAULT '{}',
target_user_ids TEXT[] DEFAULT '{}',
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
CREATE TABLE feature_flag_overrides (
id TEXT PRIMARY KEY,
flag_name TEXT REFERENCES feature_flags(name),
tenant_id TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
created_at TIMESTAMP DEFAULT now(),
UNIQUE(flag_name, tenant_id)
);
Server-side evaluation
interface FlagContext {
userId: string;
tenantId: string;
plan: string;
}
class FeatureFlagService {
private cache: Map<string, FeatureFlag> = new Map();
async isEnabled(flagName: string, context: FlagContext): Promise<boolean> {
const flag = await this.getFlag(flagName);
if (!flag) return false;
// Check tenant-level override first
const override = await this.getOverride(flagName, context.tenantId);
if (override !== null) return override;
// Global kill switch
if (!flag.enabled) return false;
// Plan-based targeting
if (flag.targetPlans.length > 0 && !flag.targetPlans.includes(context.plan)) {
return false;
}
// Specific user targeting (beta testers)
if (flag.targetUserIds.includes(context.userId)) return true;
// Percentage-based rollout (deterministic hash)
if (flag.rolloutPercentage < 100) {
const hash = this.hashUserId(context.userId, flagName);
return hash < flag.rolloutPercentage;
}
return true;
}
private hashUserId(userId: string, flagName: string): number {
const str = `${userId}:${flagName}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash) % 100;
}
}
React hook for the frontend
import { createContext, useContext } from 'react';
const FeatureFlagContext = createContext<Record<string, boolean>>({});
export function useFeatureFlag(flagName: string): boolean {
const flags = useContext(FeatureFlagContext);
return flags[flagName] ?? false;
}
// Usage in components:
function PricingPage() {
const hasNewPricing = useFeatureFlag('new-pricing-page');
if (hasNewPricing) {
return <NewPricingPage />;
}
return <CurrentPricingPage />;
}
Admin dashboard endpoint
// API route: PATCH /api/admin/feature-flags/:name
export async function updateFlag(req: Request) {
const { name } = req.params;
const { enabled, rolloutPercentage, targetPlans } = req.body;
await db.featureFlags.update({
where: { name },
data: {
enabled,
rolloutPercentage,
targetPlans,
updatedAt: new Date(),
},
});
// Invalidate cache across all instances
await redis.publish('feature-flag-update', JSON.stringify({ name }));
// Log the change for audit trail
await db.auditLog.create({
data: {
action: 'feature_flag_updated',
resource: name,
changes: { enabled, rolloutPercentage, targetPlans },
userId: req.user.id,
},
});
return Response.json({ success: true });
}
Build vs buy: when to choose an external service
Build it yourself when:
- You need a simple setup (< 20 flags)
- You want full control over the data
- You don't want extra costs for an external service
- Your flags are primarily plan-based
Use a service when:
- You need complex targeting rules
- You want A/B testing with statistical significance
- Your team is growing and you need governance
- You want real-time analytics per flag
Popular options:
- LaunchDarkly — the industry standard, powerful but pricey
- Unleash — open-source, self-hostable
- PostHog — combines feature flags with product analytics
- Flagsmith — open-source with a good free tier
Best practices for feature flags in production
1. Naming and organization
Use a consistent naming convention:
release/new-dashboard → temporary release flag
experiment/pricing-v2 → A/B test
ops/maintenance-mode → operational toggle
permission/advanced-reports → plan-based feature
2. Clean up after use
Feature flags that are permanently on are technical debt. Plan a cleanup cycle:
// Add an expiration date to your flags
interface FeatureFlag {
name: string;
expiresAt?: Date; // Alert when this date has passed
owner: string; // Who is responsible?
}
// In your CI/CD pipeline: warn about expired flags
const expiredFlags = await db.featureFlags.findMany({
where: {
expiresAt: { lt: new Date() },
enabled: true,
},
});
if (expiredFlags.length > 0) {
console.warn(`⚠️ ${expiredFlags.length} feature flags have expired and need cleanup`);
}
3. Monitoring and alerting
Connect your feature flags to your monitoring:
- Track error rates per flag variant
- Monitor performance impact of new features
- Set up automatic rollback on elevated error rates
4. Document your flags
Maintain an overview of active flags, their purpose, and owner. This prevents flags from becoming orphaned in your codebase.
5. Test both paths
Make sure your CI/CD pipeline tests both variants — with the flag on and off. Otherwise, you'll only discover bugs when disabling a flag.
A real-world scenario
Imagine: your SaaS has built a new AI-powered search system. Instead of a big-bang release:
- Week 1: Deploy with flag off. Code is in production, but nobody sees it.
- Week 2: Enable the flag for your internal team. Test in the real production environment.
- Week 3: Activate for 5% of your users. Monitor latency and relevance.
- Week 4: Scale up to 25%. Collect feedback.
- Week 5: 100% rollout. Remove the old search code and the feature flag.
If at any point latency spikes or users complain, you roll back the flag. No hotfix, no panic, no new deployment.
Conclusion
Feature flags aren't just a development tool — they're a business tool. They give you the control to launch features at the right time, for the right users, in the right way.
Start small: implement a simple flag system for your next feature. You'll find that it transforms your entire release process. Ship faster, less risk, more control.
Want help implementing feature flags in your SaaS? Get in touch for a no-obligation conversation about your architecture.