If you're a bootstrapped SaaS founder, you probably don't have a QA team. Maybe you don't even have a second developer. Every deployment is a calculated risk: will this new feature work in production, or will it break something for your 200 paying customers?
Feature flags are the safety net that lets you ship with confidence — even as a solo developer. In this article, I'll walk through a complete feature flag strategy that costs nothing, takes an afternoon to implement, and has saved me (and the founders I advise) from countless 2 AM debugging sessions.
What Are Feature Flags (and Why You Need Them)
A feature flag (also called a feature toggle) is a simple conditional check in your code that determines whether a feature is visible or active for a given user.
// Without a feature flag — risky deployment
app.get('/reports/export', exportController.generateReport);
// With a feature flag — safe deployment
app.get('/reports/export', (req, res) => {
if (featureFlags.isEnabled('report_export', req.user.id)) {
return exportController.generateReport(req, res);
}
return res.status(404).send('Not found');
});
That's it. The feature lives in your codebase, is deployed to production, but is invisible to users until you flip the switch.
Why This Matters for Bootstrapped Founders
| Without Flags | With Flags |
|---|---|
| Deploy = all-or-nothing risk | Deploy = invisible until you're ready |
| Can't test in production safely | Test with real data, real infrastructure |
| Rollback requires redeployment | Rollback = flip a boolean (instant) |
| Can't selectively release to beta users | Release to 5 users, then 50, then all |
| Feature branches accumulate merge conflicts | Features merge to main continuously |
The Three-Tier Flag System
Not all feature flags are created equal. I recommend a three-tier system that matches the maturity of your features:
Tier 1: Release Flags (Short-Lived)
Used for safely deploying new features. The flag wraps the feature during development and early release, then is removed once the feature is fully rolled out and stable.
Lifecycle: Develop → Deploy (off) → Beta test (on for 5%) →
Gradual rollout (25% → 50% → 100%) → Remove flag
Typical lifespan: 2–6 weeks
// Release flag example
if (flags.check('new_dashboard_v2', userId)) {
renderNewDashboard();
} else {
renderOldDashboard();
}
Tier 2: Experiment Flags (Medium-Lived)
Used for A/B testing or gradual migrations. These flags stay around longer because you're comparing two versions.
Lifecycle: Hypothesis → Implement → Split traffic →
Measure → Winner stays → Remove flag
Typical lifespan: 1–3 months
Tier 3: Ops Flags (Long-Lived)
Used for operational control — killing non-critical features under load, enabling maintenance modes, or gating premium features behind plan tiers.
Lifecycle: Permanent (or until the feature is removed)
// Ops flag: kill switch for heavy operations
if (flags.check('bulk_processing_enabled')) {
processBulkQueue();
} else {
// Skip processing, log for later
logger.info('Bulk processing disabled via ops flag');
}
Implementation: A Minimal Feature Flag System
You don't need LaunchDarkly or a $500/month enterprise tool. Here's a minimal implementation that works for solo and small-team SaaS:
Option A: Database-Backed Flags (Recommended for SaaS)
// flags.js — A minimal feature flag manager
const flags = {
async isEnabled(flagName, userId = null) {
const flag = await db.query(
'SELECT * FROM feature_flags WHERE name = $1',
[flagName]
);
if (!flag || !flag.enabled) return false;
// Global flag — on for everyone
if (flag.rollout_percentage >= 100) return true;
// Percentage-based rollout using user ID hash
if (userId) {
const hash = simpleHash(userId + flagName);
return (hash % 100) < flag.rollout_percentage;
}
// Specific user allowlist
if (flag.allowed_users?.includes(userId)) return true;
return false;
}
};
// Deterministic hash so the same user always gets the same experience
function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
module.exports = flags;
The Database Schema
CREATE TABLE feature_flags (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
enabled BOOLEAN DEFAULT FALSE,
rollout_percentage INTEGER DEFAULT 0,
allowed_users JSONB DEFAULT '[]',
tier VARCHAR(20) DEFAULT 'release', -- release | experiment | ops
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Example: Roll out new export feature to 10% of users
INSERT INTO feature_flags (name, description, enabled, rollout_percentage, tier)
VALUES ('csv_export_v2', 'New CSV export with custom columns', true, 10, 'release');
Option B: Config File Flags (For Simpler Setups)
If you're early stage and don't want database overhead:
// flags.json
{
"flags": {
"new_dashboard": {
"enabled": true,
"rollout_percentage": 25,
"allowed_users": ["user_123", "user_456"]
},
"bulk_import": {
"enabled": false,
"rollout_percentage": 0
},
"maintenance_mode": {
"enabled": false,
"rollout_percentage": 0,
"tier": "ops"
}
}
}
Note: Config file flags require a redeployment to change. Use this only for very early stage products or for ops flags that rarely change.
The Safe Deployment Workflow
Here's the workflow I recommend for every new feature:
Phase 1: Develop and Merge (Flag Off)
1. Create the feature behind a flag (enabled = false)
2. Merge to main branch
3. Deploy to production
4. Verify deployment succeeded (no errors, existing features still work)
The feature is in production but invisible. Zero risk to existing users.
Phase 2: Self-Test (Flag On for You Only)
1. Add your own user ID to allowed_users
2. Test the feature with real production data
3. Check edge cases, performance, error handling
4. Fix issues directly on main (no long-lived branches)
Phase 3: Beta Test (5–10 Users)
1. Invite 5–10 power users via email
2. Add their IDs to allowed_users
3. Collect feedback for 3–5 days
4. Fix critical issues before wider rollout
Phase 4: Gradual Rollout
1. Set rollout_percentage to 10
2. Monitor error rates and support tickets for 48 hours
3. If stable, increase to 25 → 50 → 100 over 1–2 weeks
4. At 100%, leave the flag for one more week as a safety net
5. Remove the flag from code and database
Rollback Protocol
If anything goes wrong at any phase:
-- Instant rollback: disable the flag
UPDATE feature_flags SET enabled = false WHERE name = 'csv_export_v2';
No redeployment. No git revert. No panicked 2 AM SSH session. Just a single database update and the feature is gone.
Monitoring Flagged Features
When you ship behind a flag, you need to monitor differently. Here's what to track:
// Wrap your feature with monitoring
app.post('/api/export', async (req, res) => {
const flagEnabled = await flags.isEnabled('csv_export_v2', req.user.id);
// Log flag evaluation
metrics.increment('flag.csv_export_v2.evaluated', {
enabled: flagEnabled.toString()
});
if (!flagEnabled) {
return res.status(404).json({ error: 'Not available' });
}
const startTime = Date.now();
try {
const result = await exportService.generate(req.body);
// Track feature performance
metrics.timing('flag.csv_export_v2.response_time', Date.now() - startTime);
metrics.increment('flag.csv_export_v2.success');
res.json(result);
} catch (err) {
metrics.increment('flag.csv_export_v2.error');
logger.error('Export v2 error', { error: err.message, userId: req.user.id });
res.status(500).json({ error: 'Export failed' });
}
});
Key Metrics to Watch
| Metric | What It Tells You | Red Flag |
|---|---|---|
| Error rate (flagged vs. baseline) | Is the new feature introducing errors? | Error rate > 2x baseline |
| Response time (flagged vs. baseline) | Is the feature slower than expected? | Response time > 1.5x baseline |
| Support tickets mentioning the feature | Are users confused or hitting issues? | > 3 tickets in 48 hours |
| Feature adoption rate | Are users actually using it? | < 10% of eligible users in week 1 |
Feature Flag Anti-Patterns to Avoid
Anti-Pattern 1: Flag Spaghetti
When you have 50+ flags, your code becomes unreadable. Every code path has 3 conditional branches.
Fix: Enforce a 2-week cleanup policy. After a flag reaches 100% rollout, create a ticket to remove it. No exceptions.
Anti-Pattern 2: Using Flags for Environment Configuration
Feature flags are for features, not for environment-specific config (API URLs, database connections). Use environment variables for those.
Anti-Pattern 3: Nested Flags
// Don't do this
if (flags.check('feature_a')) {
if (flags.check('feature_b')) {
if (flags.check('feature_c')) {
// Nobody knows what combination of flags leads here
}
}
}
Fix: One flag per feature. If features are interdependent, create a single flag that represents the combination.
Anti-Pattern 4: Forgetting to Test the "Off" Path
You tested the feature with the flag on. But did you test what happens when the flag is off? Users who don't have the flag should have a seamless experience — no broken links, no error messages, no half-rendered UI.
The Solo Founder's Feature Flag Checklist
Before deploying any new feature:
- [ ] Feature is wrapped in a flag (enabled = false)
- [ ] Flag name is descriptive (e.g.,
csv_export_v2, notflag_12) - [ ] The "off" path is tested and works correctly
- [ ] Monitoring/metrics are in place for the flagged feature
- [ ] Rollback plan is documented (one SQL query or config change)
- [ ] Rollout plan is defined (which users first, what percentage steps)
After deployment:
- [ ] Verified deployment didn't break existing features
- [ ] Tested the feature with your own account
- [ ] Invited beta users and collected feedback
- [ ] Gradually increased rollout percentage
- [ ] Monitored error rates and support tickets at each step
- [ ] Scheduled flag removal for after 100% rollout
Cost-Benefit Summary
| Investment | Cost | Return |
|---|---|---|
| Initial implementation | 1 afternoon (2–4 hours) | Safe deployments forever |
| Per-feature overhead | ~15 min to add a flag | Eliminates deployment anxiety |
| Flag maintenance | ~5 min/month cleanup | Prevents flag spaghetti |
| Monitoring setup | ~30 min one-time | Early detection of issues |
Feature flags are one of the highest-ROI investments a bootstrapped SaaS founder can make. They cost almost nothing, take an afternoon to implement, and transform your deployment process from "hold your breath and hope" to "ship, observe, and adjust."
Start with one feature. Add a flag. Deploy it. Test it in production. Flip the switch. Once you feel the safety of that workflow, you'll never go back to naked deployments again.
Top comments (0)