Fair Supply LogoFair Supply - Docs

Feature Flags & Cleanup

Guidelines for shipping fast with feature flags using Vercel Flags SDK.

Why Feature Flags?

  • Ship incomplete features to main without 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

TypeLifespanUse Case
Release flagDays to weeksHide incomplete features until ready
Ops flagPermanentKill switches, maintenance mode
Experiment flagWeeksA/B tests, user research
Permission flagPermanentPremium features, account tiers

Naming Convention

[type]-[feature]-[action]

release-spotlight-export
ops-analytics-disable
experiment-onboarding-v2
permission-bulk-actions

Setup

Install Dependencies

pnpm add @vercel/flags

Configure 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 TypeMax LifespanCleanup Trigger
Release2 weeks after 100% rolloutFeature stable in production
Experiment4 weeksExperiment concluded
OpsNeverKeep permanently
PermissionNeverKeep 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 available

Dead 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

  1. Search for usages - grep -r "functionName" .
  2. Check exports - Is it exported from a package?
  3. Run tests - Does anything break?
  4. 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:

FlagTypeOwnerCreatedStatus
release-spotlight-exportRelease@aliceJan 15Testing
experiment-onboarding-v2Experiment@bobJan 1050% rollout
ops-analytics-disableOps@opsDec 1Permanent

Resources