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 errorAdmin 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 foundPerformance
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)
- Navigate to
/dashboard/admin/feature-flags - Click "Add Flag"
- Enter key:
enableBetaFeature - Set value and type
- Save
Best Practices
✅ Do
- Use descriptive flag names (
isRegisterEnablednotreg) - 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 accountsTroubleshooting
Flags Not Updating
- Check Vercel Edge Config dashboard
- Verify
EDGE_CONFIGenvironment variable - Wait up to 30 seconds for global propagation
- Check admin UI shows "Can Write: true"
Cannot Edit Flags
- Ensure
VERCEL_API_TOKENis set - Ensure
VERCEL_EDGE_CONFIG_IDis 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