Skip to main content

How to Add Feature Flags to Your SaaS Starter (2026)

·StarterPick Team
feature-flagsnextjsguidedevops2026

TL;DR

Feature flags let you ship code without shipping features. For SaaS, they serve three purposes: plan-based gating (Pro only), progressive rollout (10% → 100%), and A/B testing. You can build a lightweight flags system in half a day or adopt a managed service like Unleash or Growthbook. This guide covers both paths.


For most SaaS apps, feature flags are just a database table and a helper function:

// prisma/schema.prisma
model FeatureFlag {
  id          String   @id @default(cuid())
  name        String   @unique
  enabled     Boolean  @default(false)
  enabledFor  String[] // user IDs or organization IDs
  rolloutPct  Int      @default(0) // 0-100 percentage rollout
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}
// lib/flags.ts
import { prisma } from './prisma';

export async function isFeatureEnabled(
  flagName: string,
  userId?: string
): Promise<boolean> {
  const flag = await prisma.featureFlag.findUnique({
    where: { name: flagName },
  });

  if (!flag) return false;
  if (!flag.enabled) return false;

  // Specific user override
  if (userId && flag.enabledFor.includes(userId)) return true;

  // Percentage rollout (deterministic per user)
  if (flag.rolloutPct > 0 && userId) {
    const hash = simpleHash(userId + flagName);
    return hash % 100 < flag.rolloutPct;
  }

  // Fully enabled
  return flag.rolloutPct === 100;
}

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash;
  }
  return Math.abs(hash);
}

Plan-Based Feature Gating

The most common SaaS use case — gate features by subscription:

// lib/plan-features.ts
const PLAN_FEATURES: Record<string, string[]> = {
  free: ['dashboard', 'basic_analytics'],
  pro: ['dashboard', 'basic_analytics', 'advanced_analytics', 'api_access', 'team_members'],
  enterprise: ['*'], // All features
};

export function planHasFeature(planName: string, feature: string): boolean {
  const features = PLAN_FEATURES[planName] ?? [];
  return features.includes('*') || features.includes(feature);
}

// In your API routes
export async function requireFeature(userId: string, feature: string) {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    include: { subscription: true },
  });

  const plan = user?.subscription?.priceId
    ? getPlanFromPriceId(user.subscription.priceId)
    : 'free';

  if (!planHasFeature(plan, feature)) {
    throw new Error(`Feature '${feature}' requires ${getRequiredPlan(feature)} plan`);
  }
}

Using Flags in Next.js

Server Components

// app/dashboard/analytics/page.tsx
import { isFeatureEnabled } from '@/lib/flags';
import { getServerSession } from 'next-auth';

export default async function AnalyticsPage() {
  const session = await getServerSession();
  const hasAdvancedAnalytics = await isFeatureEnabled(
    'advanced_analytics',
    session?.user.id
  );

  return (
    <div>
      <BasicAnalytics />
      {hasAdvancedAnalytics && <AdvancedAnalytics />}
    </div>
  );
}

Client Components with Context

// providers/FlagsProvider.tsx
'use client';
import { createContext, useContext } from 'react';

type FlagsContextType = Record<string, boolean>;
const FlagsContext = createContext<FlagsContextType>({});

export function FlagsProvider({
  flags,
  children,
}: {
  flags: FlagsContextType;
  children: React.ReactNode;
}) {
  return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
}

export function useFlag(name: string): boolean {
  const flags = useContext(FlagsContext);
  return flags[name] ?? false;
}

// Load flags in root layout and pass to provider
// app/layout.tsx
const session = await getServerSession();
const flags = session?.user.id
  ? await getUserFlags(session.user.id)
  : {};

Option 2: Managed Flag Services

When you need targeting rules, analytics, and team collaboration:

ServiceFree TierHighlights
GrowthbookSelf-host freeA/B testing built-in, open source
UnleashSelf-host freeEnterprise feature parity
Flagsmith50K requests/moSimple API, good SDKs
LaunchDarkly1 seat freeBest-in-class, expensive at scale

Growthbook (Self-hosted, Free)

// lib/growthbook.ts
import { GrowthBook } from '@growthbook/growthbook';

export function createGrowthBook(userId: string, attributes: Record<string, unknown>) {
  const gb = new GrowthBook({
    apiHost: process.env.GROWTHBOOK_API_HOST!,
    clientKey: process.env.GROWTHBOOK_CLIENT_KEY!,
    attributes: {
      id: userId,
      ...attributes,
    },
  });
  return gb;
}

// Usage
const gb = createGrowthBook(user.id, {
  plan: user.subscription?.plan,
  country: user.country,
});
await gb.loadFeatures();

const showNewDashboard = gb.isOn('new_dashboard');

Admin UI for Flag Management

A simple admin page to toggle flags without code deploys:

// app/admin/flags/page.tsx
export default async function FlagsAdminPage() {
  const flags = await prisma.featureFlag.findMany({
    orderBy: { name: 'asc' },
  });

  return (
    <div>
      <h1>Feature Flags</h1>
      {flags.map(flag => (
        <div key={flag.id} className="flex items-center justify-between py-3 border-b">
          <div>
            <p className="font-medium">{flag.name}</p>
            <p className="text-sm text-gray-500">Rollout: {flag.rolloutPct}%</p>
          </div>
          <form action={toggleFlag}>
            <input type="hidden" name="id" value={flag.id} />
            <input type="hidden" name="enabled" value={String(!flag.enabled)} />
            <button type="submit" className={flag.enabled ? 'bg-green-500' : 'bg-gray-300'}>
              {flag.enabled ? 'ON' : 'OFF'}
            </button>
          </form>
        </div>
      ))}
    </div>
  );
}

Time Budget

ComponentDuration
DB schema + migration0.5 hour
isFeatureEnabled helper1 hour
Plan-based gating1 hour
Server/client component usage1 hour
Admin toggle UI2 hours
Total~1 day

Find boilerplates with feature flags built-in on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments