Feature Flag System Architecture
Overview
The Next.js monorepo implements a feature flag system that enables controlled feature rollouts, A/B testing, and runtime configuration management across all applications. The system integrates LaunchDarkly as the primary feature flag provider with Vercel Edge Config for enhanced performance and caching.
System Architecture
The feature flag system architecture illustrates three interconnected workflows that enable dynamic feature control across the Charles Schwab applications. The Feature Flag Flow diagram below shows the runtime request lifecycle, where client requests pass through Next.js middleware and the withFlags middleware for flag precomputation, evaluation via LaunchDarkly SDK and Vercel Edge Config, ultimately resulting in URL rewriting and application responses.
The Flag Management workflow demonstrates how flags are configured through the LaunchDarkly dashboard, synchronized with environment variables and Edge Config, and evaluated at runtime. The Development Workflow ensures type safety and code quality through flag declaration, TypeScript type generation, registration, code generation, and static analysis.
This integrated approach provides both runtime flexibility and development-time safety, enabling teams to deploy features confidently with fine-grained control over their exposure to users.
Core Components
The feature flag system is built on three foundational components that work together to provide a type-safe, performant, and maintainable flag management system.
1. Flag Declaration System
Location: packages/utilities/src/flags/index.ts
The flag declaration system provides a centralized factory pattern that ensures consistency across all feature flags in the monorepo. It abstracts LaunchDarkly adapter configuration and identity resolution, allowing developers to focus on flag logic rather than integration details.
Key Responsibilities:
Architecture Pattern:
| Component | Purpose | Benefit |
|---|---|---|
| Adapter Factory | Creates LaunchDarkly adapters with environment config | Single source of truth for SDK configuration |
| Identity Function | Resolves user context from cookies/headers | Consistent user targeting across all flags |
| Type Wrapper | Wraps flags with TypeScript generics | Compile-time type safety for flag values |
| Default Handler | Manages fallback values when evaluation fails | Graceful degradation |
Integration Points:
- Environment Variables:
LAUNCHDARKLY_PROJECT_KEY,LAUNCHDARKLY_CLIENTSIDE_ID,EDGE_CONFIG_FLAGS - SDK Dependencies:
@flags-sdk/launchdarkly,flags/next - Identity Resolution: Cookie and header-based user identification
2. Individual Flag Definition
Each feature flag is defined in its own TypeScript file following a standardized template pattern.
Flag File Structure:
| Element | Purpose | Example |
|---|---|---|
| Flag Key | Unique identifier matching LaunchDarkly | 'featureCheckBotIdModes' |
| Default Value | Fallback when LaunchDarkly unavailable | false, 'off', 0 |
| Description | Human-readable purpose | 'Feature: Bot detection modes' |
| Type Parameter | TypeScript type for flag value | <boolean>, <string>, <number> |
Naming Conventions:
- File Name:
camelCase.ts(e.g.,featureCheckBotIdModes.ts) - Variable Name: Same as file name (e.g.,
featureCheckBotIdModes) - Export: Default export + named export for flexibility
Location Pattern: apps/<app-name>/src/flags/flags/<flagName>.ts
3. Flag Aggregation and Export
The aggregation layer automatically discovers and exports all flags using Node.js require.context(), eliminating manual registration and reducing human error.
Discovery Architecture:
Aggregation Benefits:
| Benefit | Description | Impact |
|---|---|---|
| Zero Configuration | Flags automatically discovered | No manual imports needed |
| Type Inference | TypeScript infers complete flag union | Full IntelliSense support |
| Build Validation | Missing flags caught at build time | Prevents runtime errors |
| Scalability | Add flags without touching index | Reduces merge conflicts |
Export Pattern: apps/<app-name>/src/flags/index.ts
Generated Artifacts:
- Flag Array: All flags for the application
- Type Definitions: Union types for flag keys
- Permutation Input: Used by
generatePermutations()for static variants
Middleware Integration
withFlags Middleware
The withFlags middleware sits in the Next.js middleware chain and handles flag precomputation and URL rewriting. This enables static generation of pages with different flag combinations while maintaining a clean URL structure.
Middleware Flow:
Middleware Responsibilities:
| Responsibility | Implementation | Purpose |
|---|---|---|
| Flag Precomputation | precompute(allFlags) | Generate unique code for flag combination |
| URL Rewriting | /${code}${pathname} | Route to correct static variant |
| Context Passing | Return { code, newUrl } | Pass flag context to application |
| Performance | Edge-compatible code | Fast execution at CDN edge |
URL Structure:
Original: /education/articles/investment-basics
Rewritten: /abc123/education/articles/investment-basics
^^^^^^^ flag permutation code
Integration Points:
- Input: Next.js
NextRequestobject - Output: Permutation code and rewritten URL
- Dependencies:
flags/nextSDK,allFlagsexport - Execution: Runs on every request at edge locations
Flag Evaluation Methods
The system provides two distinct evaluation methods optimized for different rendering strategies in Next.js.
1. Static Flag Evaluation
Use Case: Server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR)
Architectural Pattern:
Method Characteristics:
| Aspect | Description | Benefit |
|---|---|---|
| Evaluation Time | Build time or during SSR/ISR | Eliminates runtime evaluation overhead |
| Cache Strategy | Code-based lookup in precomputed map | O(1) lookup performance |
| Type Safety | Generic parameter <T> for type inference | Compile-time type checking |
| Fallback | Returns undefined if flag not found | Safe degradation |
Parameters:
flags: Complete flag array from appcode: Permutation code from middlewareflagKey: String identifier for specific flag
Return: Promise<T | undefined> where T is flag value type
2. Dynamic Flag Evaluation
Use Case: API routes, server actions, runtime-evaluated features, client-side features
Architectural Pattern:
Method Characteristics:
| Aspect | Description | Benefit |
|---|---|---|
| Evaluation Time | Request time | Real-time flag changes without rebuild |
| LaunchDarkly Query | Direct SDK call per evaluation | Always current flag state |
| User Targeting | Evaluates per-user context | Personalized experiences |
| Performance | Cached by Edge Config | Fast evaluation at edge |
Parameters:
flags: Complete flag array from appflagKey: String identifier for specific flag
Return: Promise<T | null> where T is flag value type
Comparison Matrix:
| Criteria | Static Evaluation | Dynamic Evaluation |
|---|---|---|
| Speed | Fastest (O(1) lookup) | Fast (cached SDK call) |
| Freshness | Build-time snapshot | Real-time values |
| User Targeting | Not applicable | Full targeting support |
| Use Cases | SSG, ISR, SSR pages | API routes, server actions |
| Cache Strategy | Precomputed permutations | Edge Config + LaunchDarkly |
LaunchDarkly Integration
The system integrates with LaunchDarkly through two complementary approaches, balancing performance with functionality.
1. Server-Side SDK Integration
Architecture Overview:
SDK Integration Pattern:
| Component | Purpose | Implementation |
|---|---|---|
| Singleton Client | Single SDK instance per process | Prevents multiple connections |
| Lazy Initialization | Create client on first use | Faster cold starts |
| Connection Pooling | Reuse streaming connection | Reduced API calls |
| Graceful Degradation | Default values on failure | Application resilience |
Connection Lifecycle:
- Initialization: Create client with SDK key
- Handshake: Connect to LaunchDarkly streaming API
- Configuration Download: Receive all flag definitions
- Ready State: Accept evaluation requests
- Updates: Receive real-time flag changes via stream
Environment Configuration:
LAUNCHDARKLY_SDK_KEY: Server-side SDK authentication keyLAUNCHDARKLY_PROJECT_KEY: Project identifierLAUNCHDARKLY_ENVIRONMENT: Environment (production, staging, etc.)
2. Vercel Edge Config Integration
Architecture Overview:
Edge Config Benefits:
| Benefit | Description | Impact |
|---|---|---|
| CDN Distribution | Flags cached globally at edge locations | Ultra-low latency (<10ms) |
| Reduced API Calls | LaunchDarkly queried only on cache miss | Lower costs, better performance |
| Automatic Sync | Vercel syncs flags from LaunchDarkly | No manual cache management |
| Fallback Support | SDK gracefully falls back to LaunchDarkly | High availability |
Sync Process:
- Flag Change: Developer updates flag in LaunchDarkly
- Webhook Trigger: LaunchDarkly notifies Vercel
- Edge Config Update: Vercel updates distributed cache
- Propagation: Changes propagate to all edge locations
- Application Use: Next.js apps use updated flags
Configuration:
EDGE_CONFIG_FLAGS: Connection string for Edge Config storeLAUNCHDARKLY_CLIENTSIDE_ID: Client-side ID for Edge SDK
Performance Comparison:
| Method | Latency | Cost | Freshness |
|---|---|---|---|
| Direct LaunchDarkly | 50-200ms | High | Real-time |
| Edge Config | 1-10ms | Low | Near real-time (~1 min delay) |
| Hybrid (Recommended) | 1-10ms | Low | Near real-time |
User Context and Targeting
Identity Resolution
The identity resolution system determines user context for personalized flag targeting, balancing user identification with privacy concerns.
Identity Resolution Flow:
Identity Sources:
| Source | Priority | Purpose | Format |
|---|---|---|---|
| Header | 1 (Highest) | Server-to-server identification | x-schwab-external-id |
| Cookie | 2 | Browser-based identification | schwab-external-id |
| Anonymous | 3 (Fallback) | No user identification | Empty string '' |
Context Structure:
interface LDContext {
key: string; // User identifier
anonymous?: boolean; // Whether user is identified
custom?: { // Optional custom attributes
[key: string]: any;
};
}
Targeting Capabilities:
Deduplication Strategy:
The dedupe() wrapper ensures identity resolution happens only once per request, even if multiple flags are evaluated:
- First Call: Resolves identity from headers/cookies
- Subsequent Calls: Returns cached context
- Cache Scope: Per-request (not shared across requests)
- Benefits: Reduced overhead, consistent context
Privacy Considerations:
- Minimal PII: Only external ID used (no names, emails, etc.)
- Anonymous Support: Graceful handling of unauthenticated users
- Secure Transmission: All context data encrypted in transit
- Compliance: Aligns with privacy policies
Environment Configuration
Required Environment Variables
# LaunchDarkly Configuration
LAUNCHDARKLY_SDK_KEY=sdk-xxxxx-xxxxx-xxxxx
LAUNCHDARKLY_API_KEY=api-xxxxx-xxxxx-xxxxx
LAUNCHDARKLY_PROJECT_KEY=project-key
LAUNCHDARKLY_ENVIRONMENT=production
LAUNCHDARKLY_CLIENTSIDE_ID=clientside-id
# Vercel Edge Config
EDGE_CONFIG_FLAGS=https://edge-config.vercel.com/xxxxx
# Security
FLAGS_SECRET=secure-secret-key
Application-Specific Implementations
1. www.schwab.com
- Flag Count: 20+ feature flags
- Key Features: Bot detection, content variations, A/B testing
- Integration: Full LaunchDarkly integration with Edge Config caching
2. client.schwab.com
- Flag Count: 15+ feature flags
- Key Features: Client portal features, UI variations
- Integration: Dynamic flag evaluation with client context
3. meganav-mfe
- Flag Count: 10+ feature flags
- Key Features: Navigation features, responsive design toggles
- Integration: Micro-frontend flag synchronization
Testing and Mocking
Test Utilities
// packages/test/src/mock/base/Mockery.ts
export class Mockery {
static mockEvaluateStaticFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateStaticFlag: () => Promise.resolve(flags),
}));
}
static mockEvaluateDynamicFlag(flags: Record<string, boolean> | undefined): void {
jest.doMock('@schwab/utilities/flags', () => ({
evaluateDynamicFlag: () => Promise.resolve(flags),
}));
}
}
Performance Optimization
1. Flag Precomputation
- Strategy: Precompute flag combinations at build time
- Benefit: Reduces runtime evaluation overhead
- Implementation:
generatePermutations()function creates static variants
2. Edge Config Caching
- Strategy: Cache flag configurations at CDN edge locations
- Benefit: Reduces LaunchDarkly API calls and latency
- Implementation: Vercel Edge Config with LaunchDarkly sync
3. Static Site Generation
- Strategy: Generate static pages for each flag permutation
- Benefit: Eliminates runtime flag evaluation for static content
- Implementation: URL rewriting with flag codes
Monitoring and Analytics
1. Flag Performance Tracking
- Metrics: Flag evaluation latency, cache hit rates
- Tools: LaunchDarkly analytics, Vercel analytics
- Alerts: Flag evaluation errors, timeout thresholds
2. A/B Test Analytics
- Integration: Tealium analytics for flag-based experiments
- Tracking: Conversion rates, user engagement metrics
- Reporting: LaunchDarkly experimentation dashboard
Development Workflow
1. Creating New Flags
# 1. Create flag file using template
cp packages/utilities/src/flags/_template.txt apps/app-name/src/flags/flags/newFlag.ts
# 2. Update flag declaration
# Edit the new flag file with proper key, defaultValue, description
# 3. Flag is automatically discovered via require.context
# No manual registration required
2. Flag Lifecycle Management
- Development: Create flag with
defaultValue: false - Testing: Enable for specific user segments
- Rollout: Gradually increase percentage
- Cleanup: Remove flag code after full rollout
3. Code Generation
- Automatic Discovery: Flags are discovered via
require.context() - Type Safety: TypeScript interfaces ensure type-safe flag usage
- Build Integration: Flags are validated at build time
Security Considerations
1. Access Control
- LaunchDarkly RBAC: Role-based access control for flag management
- Environment Separation: Strict environment isolation
- API Key Rotation: Regular rotation of LaunchDarkly API keys
2. Data Protection
- User Context: Minimal PII in flag targeting context
- Encryption: All flag data encrypted in transit and at rest
- Audit Logging: Complete audit trail for flag changes
Dependencies
Core Dependencies
| Package | Version | Purpose |
|---|---|---|
@flags-sdk/launchdarkly | ^0.3.2 | LaunchDarkly integration SDK |
@launchdarkly/vercel-server-sdk | ^1.3.34 | Vercel-optimized LaunchDarkly SDK |
@launchdarkly/node-server-sdk | ^9.10.2 | Node.js LaunchDarkly SDK |
@vercel/edge-config | Latest | Vercel Edge Config client |
flags | ^4.0.1 | Core flag management library |
Development Dependencies
- Testing: Jest mocking utilities for flag simulation
- TypeScript: Type definitions for flag declarations
- Build Tools: Flag validation and code generation tools