Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Feature Flags

Toggle features without code changes

Overview

Feature flags allow you to enable or disable features at runtime without deploying new code. This project uses Vercel Edge Config for ultra-fast, globally-distributed feature flag management.

Architecture

┌─────────────────┐
│   Application   │
└────────┬────────┘
         │ getFeatureFlag()
         ↓
┌─────────────────┐
│ Feature Flags   │ (src/lib/feature-flags/index.ts)
│  Client Layer   │
└────────┬────────┘
         │
         ↓
┌─────────────────┐
│  Vercel Edge    │ ← Ultra-fast global storage
│     Config      │   (Falls back to defaults)
└─────────────────┘

Configuration

Feature flags are defined in src/lib/feature-flags/index.ts:

export const DEFAULT_FLAGS: Record<string, boolean | number | string> = {
  isRegisterEnabled: true,
  isLoginEnabled: true,
  isAuthProviderEnabled: true,
  disableBotIdCheck: false,
  maintenanceMode: false,
  disableAccountDeletion: false,
  disableSubmit: false,
} as const;

// Descriptions for admin UI
export const FLAG_DESCRIPTIONS: Record<FeatureFlags, string> = {
  isRegisterEnabled: "Enable/disable new user registration",
  isLoginEnabled: "Enable/disable user login",
  isAuthProviderEnabled: "Enable/disable OAuth providers (Google, etc.)",
  disableBotIdCheck: "Disable bot verification checks",
  maintenanceMode: "Put the application in maintenance mode",
  disableAccountDeletion: "Prevent users from deleting their accounts",
  disableSubmit: "Disable form submissions globally",
};

Usage

Reading Feature Flags

Use the getFeatureFlag function to check flag values:

import { getFeatureFlag } from "@/lib/feature-flags";

// In Server Components or API routes
export default async function RegisterPage() {
  const isRegisterEnabled = await getFeatureFlag("isRegisterEnabled");
  
  if (!isRegisterEnabled) {
    return <div>Registration is temporarily disabled</div>;
  }
  
  return <RegisterForm />;
}

Custom Feature Flag Functions

Create domain-specific helpers for complex logic:

import { getFeatureFlag } from "@/lib/feature-flags";
import { AuthProvider } from "@/lib/auth/definition";
import { env } from "@/env/env-server";

export async function isAuthProviderEnabled(
  provider: AuthProvider
): Promise<boolean> {
  switch (provider) {
    case "google":
      // Check both environment config AND feature flag
      return !env.GOOGLE_CLIENT_ID || !env.GOOGLE_CLIENT_SECRET 
        ? false 
        : !!(await getFeatureFlag("isAuthProviderEnabled"));
    default:
      return false;
  }
}

Type Safety

Feature flags are fully typed:

type FeatureFlags = keyof typeof DEFAULT_FLAGS;

// TypeScript ensures only valid flags are used
await getFeatureFlag("isRegisterEnabled"); // ✅ Valid
await getFeatureFlag("invalidFlag");       // ❌ Type error

Admin Management

Admins can manage feature flags through the UI at /dashboard/admin/feature-flags.

tRPC Router

The feature flags router provides CRUD operations:

export const featureFlagsRouter = createTRPCRouter({
  // List all flags
  list: roleProcedure(UserRole.ADMIN)
    .query(async () => {
      const allItems = await flagClient.getAll();
      const flags = Object.entries(allItems).map(([key, value]) => ({
        key,
        value,
        valueType: inferValueType(value),
        description: FLAG_DESCRIPTIONS[key],
      }));
      return { success: true, payload: { flags } };
    }),
  
  // Update or create flag
  upsert: roleProcedure(UserRole.ADMIN)
    .input(featureFlagUpdateSchema)
    .mutation(async ({ input }) => {
      await updateEdgeConfig([{
        operation: "upsert",
        key: input.key,
        value: input.value,
      }]);
      return { success: true, message: "Flag updated" };
    }),
  
  // Delete flag
  delete: roleProcedure(UserRole.ADMIN)
    .input(featureFlagDeleteSchema)
    .mutation(async ({ input }) => {
      await updateEdgeConfig([{
        operation: "delete",
        key: input.key,
      }]);
      return { success: true, message: "Flag deleted" };
    }),
});

Admin UI Features

  • Real-time updates - Changes propagate globally within seconds
  • Type-specific inputs - Boolean toggles, number inputs, text fields
  • Inline editing - Edit values directly in the list
  • Create new flags - Add custom flags on the fly
  • Delete flags - Remove unused flags

Write operations (create/update/delete) require VERCEL_API_TOKEN and VERCEL_EDGE_CONFIG_ID environment variables to be set.

Value Types

Feature flags support three value types:

Boolean Flags

// Definition
DEFAULT_FLAGS = {
  maintenanceMode: false,
};

// Usage
const isMaintenance = await getFeatureFlag("maintenanceMode");
if (isMaintenance) {
  return <MaintenancePage />;
}

Number Flags

// Definition
DEFAULT_FLAGS = {
  maxUploadSize: 10485760, // 10 MB
};

// Usage
const maxSize = await getFeatureFlag("maxUploadSize");
if (file.size > maxSize) {
  throw new Error("File too large");
}

String Flags

// Definition
DEFAULT_FLAGS = {
  announcementMessage: "New features coming soon!",
};

// Usage
const message = await getFeatureFlag("announcementMessage");
return <Alert>{message}</Alert>;

Environment-Based Behavior

Feature flags have intelligent fallback behavior:

Local Development

// When EDGE_CONFIG is not set
const flag = await getFeatureFlag("isRegisterEnabled");
// Returns: DEFAULT_FLAGS.isRegisterEnabled (true)

Production (with Edge Config)

// When EDGE_CONFIG is set
const flag = await getFeatureFlag("isRegisterEnabled");
// Returns: Value from Vercel Edge Config
// Falls back to default if key not found

Performance

Edge Config Benefits

  • Ultra-fast reads - Sub-millisecond response times
  • Global distribution - Replicated to all edge locations
  • No database overhead - Separate from your main database
  • Built-in caching - Automatic cache invalidation

Caching Strategy

export const flagClient = env.EDGE_CONFIG 
  ? createClient(env.EDGE_CONFIG, {
      cache: "no-store",      // Always fetch fresh data
      staleIfError: 30,       // Serve stale for 30s on error
    }) 
  : null;

Adding New Flags

Step 1: Define the Flag

Add to src/lib/feature-flags/index.ts:

export const DEFAULT_FLAGS = {
  // ... existing flags
  enableBetaFeature: false, // NEW
} as const;

export const FLAG_DESCRIPTIONS = {
  // ... existing descriptions
  enableBetaFeature: "Enable access to beta features", // NEW
};

Step 2: Use the Flag

import { getFeatureFlag } from "@/lib/feature-flags";

export default async function BetaFeaturePage() {
  const enabled = await getFeatureFlag("enableBetaFeature");
  
  if (!enabled) {
    return <ComingSoonPage />;
  }
  
  return <BetaFeature />;
}

Step 3: Set in Production (Optional)

  1. Navigate to /dashboard/admin/feature-flags
  2. Click "Add Flag"
  3. Enter key: enableBetaFeature
  4. Set value and type
  5. Save

Best Practices

✅ Do

  • Use descriptive flag names (isRegisterEnabled not reg)
  • Add descriptions for all flags
  • Define sensible defaults
  • Use flags for gradual rollouts
  • Clean up unused flags
  • Document flag purposes

❌ Don't

  • Use flags for configuration (use environment variables)
  • Create flags without defaults
  • Hard-code flag checks throughout codebase
  • Leave flags in code after full rollout
  • Use flags for sensitive security settings

Common Patterns

Maintenance Mode

export default async function RootLayout({ children }) {
  const isMaintenance = await getFeatureFlag("maintenanceMode");
  
  if (isMaintenance) {
    return <MaintenancePage />;
  }
  
  return <>{children}</>;
}

Gradual Feature Rollout

export default async function FeaturePage() {
  const betaEnabled = await getFeatureFlag("enableBetaFeature");
  
  return (
    <div>
      <StableFeature />
      {betaEnabled && <BetaFeature />}
    </div>
  );
}

A/B Testing

export default async function LandingPage() {
  const variant = await getFeatureFlag("landingPageVariant");
  
  return variant === "A" ? <VariantA /> : <VariantB />;
}

Emergency Kill Switch

export default async function FormPage() {
  const disableSubmit = await getFeatureFlag("disableSubmit");
  
  return (
    <form>
      {/* form fields */}
      <Button disabled={disableSubmit}>
        {disableSubmit ? "Submissions Disabled" : "Submit"}
      </Button>
    </form>
  );
}

Environment Variables

Required for read access:

EDGE_CONFIG="https://edge-config.vercel.com/xxx"

Required for write access (admin UI):

VERCEL_API_TOKEN="xxx"
VERCEL_EDGE_CONFIG_ID="ecfg_xxx"
VERCEL_TEAM_ID="team_xxx" # Optional, for team accounts

Troubleshooting

Flags Not Updating

  • Check Vercel Edge Config dashboard
  • Verify EDGE_CONFIG environment variable
  • Wait up to 30 seconds for global propagation
  • Check admin UI shows "Can Write: true"

Cannot Edit Flags

  • Ensure VERCEL_API_TOKEN is set
  • Ensure VERCEL_EDGE_CONFIG_ID is set
  • Verify token has correct permissions
  • Check admin role assignment

Default Values Not Working

  • Verify flag key matches DEFAULT_FLAGS
  • Check TypeScript types are correct
  • Ensure flag is defined in FLAG_DESCRIPTIONS

Next Steps

  • Configuration - Other configuration patterns
  • Environment Variables - Environment setup
  • Authorization - Role-based access control

On this page

Overview
Architecture
Configuration
Usage
Reading Feature Flags
Custom Feature Flag Functions
Type Safety
Admin Management
tRPC Router
Admin UI Features
Value Types
Boolean Flags
Number Flags
String Flags
Environment-Based Behavior
Local Development
Production (with Edge Config)
Performance
Edge Config Benefits
Caching Strategy
Adding New Flags
Step 1: Define the Flag
Step 2: Use the Flag
Step 3: Set in Production (Optional)
Best Practices
✅ Do
❌ Don't
Common Patterns
Maintenance Mode
Gradual Feature Rollout
A/B Testing
Emergency Kill Switch
Environment Variables
Troubleshooting
Flags Not Updating
Cannot Edit Flags
Default Values Not Working
Next Steps