feature flags
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user