Feature Flags & Cleanup
Guidelines for shipping fast with feature flags using Vercel Flags SDK.
Why Feature Flags?
- Ship incomplete features to
mainwithout exposing them - Decouple deployment from release - deploy anytime, release when ready
- Reduce PR size - merge work-in-progress behind a flag
- Easy rollback - disable a feature without reverting code
- A/B testing - test features with specific users or accounts
Flag Types
| Type | Lifespan | Use Case |
|---|---|---|
| Release flag | Days to weeks | Hide incomplete features until ready |
| Ops flag | Permanent | Kill switches, maintenance mode |
| Experiment flag | Weeks | A/B tests, user research |
| Permission flag | Permanent | Premium features, account tiers |
Naming Convention
[type]-[feature]-[action]
release-spotlight-export
ops-analytics-disable
experiment-onboarding-v2
permission-bulk-actionsSetup
Install Dependencies
pnpm add @vercel/flagsConfigure Flags
// apps/web/src/lib/flags.ts
import { flag } from '@vercel/flags/next';
export const spotlightExportFlag = flag({
key: 'release-spotlight-export',
decide: () => false, // Default OFF
});
export const maintenanceModeFlag = flag({
key: 'ops-maintenance-mode',
decide: () => false,
});
export const onboardingV2Flag = flag({
key: 'experiment-onboarding-v2',
decide: () => false,
});Usage
In Server Components
import { spotlightExportFlag } from '@/lib/flags';
export default async function SpotlightPage() {
const showExport = await spotlightExportFlag();
return (
<Stack>
<SpotlightDetails />
{showExport && <ExportButton />}
</Stack>
);
}In Server Actions
import { spotlightExportFlag } from '@/lib/flags';
export const exportSpotlightAction = createServerAction()
.input(exportSchema)
.handler(async ({ input }) => {
const enabled = await spotlightExportFlag();
if (!enabled) {
throw new Error('Feature not available');
}
// ... implementation
});
}In Client Components
Use a Server Component wrapper to pass the flag value:
// page.tsx (Server Component)
import { spotlightExportFlag } from '@/lib/flags';
import { SpotlightClient } from './spotlight-client';
export default async function SpotlightPage() {
const showExport = await spotlightExportFlag();
return <SpotlightClient showExport={showExport} />;
}
// spotlight-client.tsx (Client Component)
'use client';
export function SpotlightClient({ showExport }: { showExport: boolean }) {
return (
<Stack>
<Button>View Details</Button>
{showExport && <Button>Export</Button>}
</Stack>
);
}Advanced Patterns
User-Based Targeting
import { flag } from '@vercel/flags/next';
import { getUser } from '@/lib/auth';
export const betaFeatureFlag = flag({
key: 'release-beta-feature',
decide: async () => {
const user = await getUser();
// Enable for specific users or accounts
return user?.accountId === 'beta-account-id';
},
});Percentage Rollout
import { flag } from '@vercel/flags/next';
import { getUser } from '@/lib/auth';
export const experimentFlag = flag({
key: 'experiment-new-checkout',
decide: async () => {
const user = await getUser();
if (!user) return false;
// Consistent hash for same user
const hash = simpleHash(user.id);
return hash % 100 < 50; // 50% rollout
},
});
function simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
}
return Math.abs(hash);
}Override via URL (Development)
import { flag } from '@vercel/flags/next';
export const myFlag = flag({
key: 'release-my-feature',
decide: () => false,
options: [true, false],
});Then visit: /?release-my-feature=true
Vercel Toolbar Integration
Enable the Flags Explorer in development:
// apps/web/src/app/layout.tsx
import { VercelToolbar } from '@vercel/toolbar/next';
export default function RootLayout({ children }) {
const showToolbar = process.env.NODE_ENV === 'development';
return (
<html>
<body>
{children}
{showToolbar && <VercelToolbar />}
</body>
</html>
);
}Flag Lifecycle
1. CREATE → Add flag in flags.ts, default OFF
2. DEVELOP → Build feature behind flag
3. TEST → Override flag in development/preview
4. RELEASE → Change decide() to return true
5. CLEANUP → Remove flag and dead code (within 2 weeks)Cleanup Rules
Flags are technical debt. Clean them up aggressively.
| Flag Type | Max Lifespan | Cleanup Trigger |
|---|---|---|
| Release | 2 weeks after 100% rollout | Feature stable in production |
| Experiment | 4 weeks | Experiment concluded |
| Ops | Never | Keep permanently |
| Permission | Never | Keep permanently |
Cleanup Checklist
- Remove flag definition from
flags.ts - Remove flag usage from components
- Remove flag usage from server actions
- Delete any "old path" code that was replaced
- Update tests to remove flag variations
Cleanup PR Template
## Flag Cleanup: [FLAG_NAME]
**Flag type:** Release
**Created:** 2024-01-15
**Released:** 2024-01-22
**Removed:** 2024-01-29
### Changes
- Removed release-spotlight-export flag
- Deleted old export modal component
- Export is now always availableDead Code Deletion
When to Delete
Delete code immediately when:
- A feature flag is removed and the old path is no longer needed
- A feature is replaced by a new implementation
- Code is commented out (delete, don't comment)
- Code is unreachable
How to Delete Safely
- Search for usages -
grep -r "functionName" . - Check exports - Is it exported from a package?
- Run tests - Does anything break?
- Run build - TypeScript will catch missing imports
What NOT to Delete
- Public API contracts (may have external consumers)
- Database migrations (even if reverted)
- Audit log code (compliance requirement)
Anti-Patterns
Don't: Nest Flags
// BAD - hard to reason about
const flagA = await featureAFlag();
const flagB = await featureBFlag();
if (flagA) {
if (flagB) {
// ...
}
}Don't: Long-Lived Release Flags
// BAD - this flag has been here for 6 months
const showDashboard = await newDashboardFlag();Don't: Flag Entire Pages
// BAD - too coarse
const useV2 = await settingsV2Flag();
if (useV2) {
return <SettingsPageV2 />;
}
return <SettingsPage />;Do: Flag Specific Components
// GOOD - granular control
const showNotifications = await notificationSettingsFlag();
<SettingsPage>
<GeneralSettings />
{showNotifications && <NotificationSettings />}
</SettingsPage>Tracking Flags
Maintain a simple list in your project management tool:
| Flag | Type | Owner | Created | Status |
|---|---|---|---|---|
release-spotlight-export | Release | @alice | Jan 15 | Testing |
experiment-onboarding-v2 | Experiment | @bob | Jan 10 | 50% rollout |
ops-analytics-disable | Ops | @ops | Dec 1 | Permanent |