6.0 KiB
6.0 KiB
Feature Architecture: API-Driven Feature Flags
The Problem
Your previous system had two overlapping concepts that conflicted:
- Feature Flags (
features.config.ts) - Controls individual features - App Modes (
NEXT_PUBLIC_GRIDPILOT_MODE) - Controls overall platform visibility
This created confusion because:
- Development showed only landing page but feature config said everything was enabled
- It's unclear which system controls what
- Teams don't know when to use mode vs feature flag
The Solution: API-Driven Features
Single Source of Truth: API
All feature control now comes from the API /features endpoint:
// API Response (from /features endpoint)
{
"alpha_features": true,
"platform.dashboard": true,
"platform.leagues": true,
"platform.teams": true,
"sponsors.portal": true,
// ... all other features
}
FeatureFlagService - The Bridge
The FeatureFlagService is the only way to check feature availability:
// ✅ CORRECT: Use FeatureFlagService
const service = await FeatureFlagService.fromAPI();
const isEnabled = service.isEnabled('platform.dashboard');
// ❌ WRONG: No more environment mode checks
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; // REMOVED
How It Works
- API Layer:
features.config.tsdefines features per environment - Service Layer:
FeatureFlagServicefetches and caches features from API - Client Layer: Components use
FeatureFlagServiceorModeGuardfor conditional rendering
Implementation Rules
Rule 1: Always Use FeatureFlagService
// ✅ CORRECT: Use FeatureFlagService for all feature checks
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
async function MyComponent() {
const service = await FeatureFlagService.fromAPI();
if (service.isEnabled('platform.dashboard')) {
return <Dashboard />;
}
return <LandingPage />;
}
Rule 2: Use ModeGuard for Conditional Rendering
// ✅ CORRECT: Use ModeGuard for client-side conditional rendering
import { ModeGuard } from '@/components/shared/ModeGuard';
function Page() {
return (
<ModeGuard feature="platform.dashboard" fallback={<LandingPage />}>
<Dashboard />
</ModeGuard>
);
}
Rule 3: Use Hooks for Client Components
// ✅ CORRECT: Use hooks in client components
import { useFeature, useFeatures } from '@/components/shared/ModeGuard';
function ClientComponent() {
const hasDashboard = useFeature('platform.dashboard');
const features = useFeatures(['platform.dashboard', 'platform.leagues']);
if (!hasDashboard) return null;
return <div>Dashboard Content</div>;
}
Configuration Examples
API Configuration (features.config.ts)
// apps/api/src/config/features.config.ts
export const featuresConfig = {
development: {
// Alpha features enabled for development
alpha_features: 'enabled',
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
races: 'enabled',
},
sponsors: {
portal: 'enabled',
},
},
production: {
// Gradual rollout in production
alpha_features: 'disabled',
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
races: 'enabled',
},
sponsors: {
portal: 'coming_soon', // Not ready yet
},
},
};
No More Environment Variables
# ❌ REMOVED
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# ✅ ONLY NEEDED
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
Migration Path
Before (Confusing)
// apps/website/lib/mode.ts (DELETED)
export function getAppMode(): AppMode {
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
// ... complex logic
}
// apps/website/components/shared/ModeGuard.tsx (UPDATED)
// Used to check process.env.NEXT_PUBLIC_GRIDPILOT_MODE
// Now uses FeatureFlagService
After (Clear)
// apps/website/lib/feature/FeatureFlagService.ts
export class FeatureFlagService {
static async fromAPI(): Promise<FeatureFlagService> {
// Fetches from /features endpoint
// Caches results
// Provides simple isEnabled() method
}
}
// apps/website/components/shared/ModeGuard.tsx (UPDATED)
// Uses FeatureFlagService for all feature checks
// No environment variables needed
Benefits
- Single Source of Truth: API controls all features
- No Confusion: Clear separation between feature availability and platform scope
- Runtime Control: Features can be changed without redeployment
- Type Safety: Feature names are typed and validated
- Easy Testing: Mock the API response for tests
Quick Reference
| Scenario | Implementation | Result |
|---|---|---|
| Local dev | API returns all enabled features | Full platform |
| CI/CD tests | API returns all enabled features | Full platform |
| Staging | API returns controlled features | Controlled rollout |
| Production | API returns production features | Public launch |
| Feature flags | API config for granular control | Fine-tuned features |
Files Changed
- ✅
apps/website/lib/config/env.ts- RemovedNEXT_PUBLIC_GRIDPILOT_MODE - ✅
apps/website/lib/mode.ts- DELETED - ✅
apps/website/lib/mode.test.ts- DELETED - ✅
apps/website/components/shared/ModeGuard.tsx- Updated to use API - ✅
apps/website/env.d.ts- Removed mode declaration - ✅ All env example files - Updated to remove mode variable
- ✅
apps/website/components/errors/ErrorAnalyticsDashboard.tsx- Removed appMode - ✅
apps/website/components/dev/DebugModeToggle.tsx- Removed appMode - ✅
apps/website/lib/infrastructure/ErrorReplay.ts- Removed appMode
Verification
Run these commands to verify the changes:
# Type check
npm run typecheck
# Lint
npm run lint
# Tests
npm run test
# Build
npm run build
All should pass without references to NEXT_PUBLIC_GRIDPILOT_MODE or the deleted mode files.