229 lines
6.0 KiB
Markdown
229 lines
6.0 KiB
Markdown
# 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:
|
|
1. Development showed only landing page but feature config said everything was enabled
|
|
2. It's unclear which system controls what
|
|
3. 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// ✅ 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**
|
|
|
|
1. **API Layer**: `features.config.ts` defines features per environment
|
|
2. **Service Layer**: `FeatureFlagService` fetches and caches features from API
|
|
3. **Client Layer**: Components use `FeatureFlagService` or `ModeGuard` for conditional rendering
|
|
|
|
## Implementation Rules
|
|
|
|
### **Rule 1: Always Use FeatureFlagService**
|
|
|
|
```typescript
|
|
// ✅ 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**
|
|
|
|
```typescript
|
|
// ✅ 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**
|
|
|
|
```typescript
|
|
// ✅ 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)**
|
|
|
|
```typescript
|
|
// 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**
|
|
|
|
```bash
|
|
# ❌ REMOVED
|
|
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
|
|
|
# ✅ ONLY NEEDED
|
|
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
|
```
|
|
|
|
## Migration Path
|
|
|
|
### **Before (Confusing)**
|
|
|
|
```typescript
|
|
// 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)**
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Single Source of Truth**: API controls all features
|
|
2. **No Confusion**: Clear separation between feature availability and platform scope
|
|
3. **Runtime Control**: Features can be changed without redeployment
|
|
4. **Type Safety**: Feature names are typed and validated
|
|
5. **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` - Removed `NEXT_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:
|
|
|
|
```bash
|
|
# 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. |