feature flags

This commit is contained in:
2026-01-07 22:05:53 +01:00
parent 1b63fa646c
commit 606b64cec7
530 changed files with 2092 additions and 2943 deletions

View File

@@ -1,225 +1,229 @@
# Feature Architecture: Modes vs Feature Flags
# Feature Architecture: API-Driven Feature Flags
## The Problem
Your current system has two overlapping concepts that conflict:
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 creates confusion because:
1. Development shows only landing page but feature config says everything is enabled
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: Clear Separation
## The Solution: API-Driven Features
### **Two-Tier System**
### **Single Source of Truth: API**
```
┌─────────────────────────────────────────┐
│ APP MODE (Tier 1) │
│ Controls WHAT the platform shows │
│ - pre-launch: Landing page only │
│ - alpha: Full platform access │
│ - beta: Production-ready features │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ FEATURE FLAGS (Tier 2) │
│ Controls WHICH features are enabled │
│ - Individual feature toggles │
│ - Rollout control │
│ - A/B testing │
└─────────────────────────────────────────┘
```
### **Mode = Platform Scope**
**App Mode defines what the entire platform can do:**
- **`pre-launch`**: "We're not ready yet"
- Shows: Landing page, Discord CTA, FAQ
- Hides: All navigation, dashboard, leagues, teams, races
- Purpose: Marketing/teaser phase
- **`alpha`**: "Early access for testers"
- Shows: Everything + alpha badges
- Purpose: Internal testing, early adopters
- All features enabled by default
- **`beta`**: "Public beta"
- Shows: Production features only
- Purpose: Gradual rollout to real users
- Features controlled individually
### **Feature Flags = Feature Control**
**Feature flags control individual features within a mode:**
All feature control now comes from the API `/features` endpoint:
```typescript
// In alpha mode, all features are ON by default
// But you can still disable specific ones for testing
// API Response (from /features endpoint)
{
"platform.dashboard": "enabled",
"platform.leagues": "enabled",
"platform.teams": "disabled", // Testing without teams
"sponsors.portal": "enabled",
"admin.dashboard": "enabled"
"alpha_features": true,
"platform.dashboard": true,
"platform.leagues": true,
"platform.teams": true,
"sponsors.portal": true,
// ... all other features
}
```
## Simple Mental Model
### **FeatureFlagService - The Bridge**
### **For Developers: "The Restaurant Analogy"**
The `FeatureFlagService` is the **only** way to check feature availability:
```
APP MODE = Restaurant State
├── "Closed" (pre-launch) → Only show entrance/menu
├── "Soft Opening" (alpha) → Full menu, everything available
└── "Grand Opening" (beta) → Full menu, but some items may be 86'd
```typescript
// ✅ CORRECT: Use FeatureFlagService
const service = await FeatureFlagService.fromAPI();
const isEnabled = service.isEnabled('platform.dashboard');
FEATURE FLAGS = Menu Items
├── Each dish can be: Available / 86'd / Coming Soon
├── Works within whatever restaurant state you're in
└── Lets you control individual items precisely
// ❌ WRONG: No more environment mode checks
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; // REMOVED
```
### **Decision Tree**
### **How It Works**
```
Question: "What should I use?"
├─ "Is the platform ready for ANY users?"
│ ├─ No → Use APP MODE = pre-launch
│ └─ Yes → Continue...
├─ "Are we in testing or production?"
│ ├─ Testing → Use APP MODE = alpha
│ └─ Production → Use APP MODE = beta
└─ "Do I need to control a specific feature?"
└─ Yes → Use FEATURE FLAGS (regardless of mode)
```
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: Mode Controls Visibility**
### **Rule 1: Always Use FeatureFlagService**
```typescript
// ❌ WRONG: Using feature flags to hide entire platform
{
"platform": {
"dashboard": "disabled",
"leagues": "disabled",
"teams": "disabled"
// ✅ 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 />;
}
// ✅ CORRECT: Use mode for platform-wide visibility
// NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
```
### **Rule 2: Feature Flags Control Granularity**
### **Rule 2: Use ModeGuard for Conditional Rendering**
```typescript
// ✅ CORRECT: Feature flags for fine-grained control
{
"platform": {
"dashboard": "enabled",
"leagues": "enabled",
"teams": "enabled", // But specific team features...
"teams.create": "disabled", // ...can be toggled
"teams.delete": "coming_soon"
}
// ✅ 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: Alpha Mode = Auto-Enable**
### **Rule 3: Use Hooks for Client Components**
```typescript
// In alpha mode, ALL features are enabled automatically
// Feature flags can only DISABLE, not enable
// This eliminates configuration complexity
// ✅ CORRECT: Use hooks in client components
import { useFeature, useFeatures } from '@/components/shared/ModeGuard';
// Feature flag service in alpha mode:
if (mode === 'alpha') {
return new FeatureFlagService([
'all', 'features', 'enabled', 'by', 'default'
]);
function ClientComponent() {
const hasDashboard = useFeature('platform.dashboard');
const features = useFeatures(['platform.dashboard', 'platform.leagues']);
if (!hasDashboard) return null;
return <div>Dashboard Content</div>;
}
```
## Migration Path
### **Current State (Confusing)**
```
Development:
- Mode: pre-launch (default)
- Features: All enabled in config
- Result: Shows landing page only ❌
Why? Because mode overrides feature config
```
### **Target State (Clear)**
```
Development:
- Mode: alpha (explicit)
- Features: All auto-enabled
- Result: Full platform with alpha badges ✅
Production:
- Mode: beta
- Features: Controlled individually
- Result: Gradual rollout ✅
```
## Configuration Examples
### **Simple Mode Config**
```typescript
// apps/website/.env.development
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
### **API Configuration (features.config.ts)**
// apps/website/.env.production
NEXT_PUBLIC_GRIDPILOT_MODE=beta
```
### **Feature Flags (Only When Needed)**
```typescript
// Only override defaults when necessary
// apps/api/src/config/features.config.ts
{
production: {
export const featuresConfig = {
development: {
// Alpha features enabled for development
alpha_features: 'enabled',
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
races: 'enabled',
leaderboards: 'enabled'
},
sponsors: {
portal: 'enabled',
},
},
production: {
// Gradual rollout in production
alpha_features: 'disabled',
platform: {
dashboard: 'enabled',
management: 'disabled' // Not ready yet
}
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. **No Confusion**: Clear separation of concerns
2. **Less Config**: Alpha mode needs zero feature config
3. **Easy Onboarding**: New devs just set mode=alpha
4. **Powerful Control**: Feature flags still available when needed
5. **Backward Compatible**: Existing code works with new concept
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 | Mode | Feature Flags | Result |
|----------|------|---------------|--------|
| Local dev | `alpha` | None needed | Full platform |
| CI/CD tests | `alpha` | None needed | Full platform |
| Staging | `beta` | Some disabled | Controlled rollout |
| Production | `beta` | Gradual enable | Public launch |
| Marketing site | `pre-launch` | N/A | Landing page only |
| 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 |
This eliminates the contradiction and gives you clear power: **Mode for scope, flags for granularity.**
## 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.