feature flags
This commit is contained in:
334
plans/API_DRIVEN_FEATURE_CONFIG.md
Normal file
334
plans/API_DRIVEN_FEATURE_CONFIG.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# API-Driven Feature Configuration - Implementation Plan
|
||||
|
||||
## Goal
|
||||
Create a **single source of truth** for feature configuration that both API and Website use, eliminating duplication and ensuring security.
|
||||
|
||||
## Current State
|
||||
- **Website**: Uses `NEXT_PUBLIC_GRIDPILOT_MODE=alpha` with hardcoded feature list
|
||||
- **API**: Uses `NODE_ENV` with `features.config.ts`
|
||||
- **Problem**: Two systems, risk of inconsistency, security gaps
|
||||
|
||||
## Target State
|
||||
- **Single Source**: `apps/api/src/config/features.config.ts`
|
||||
- **Website**: Reads features from API endpoint
|
||||
- **Result**: One config file, automatic synchronization, secure by default
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create API Feature Endpoint
|
||||
|
||||
**File**: `apps/api/src/features/features.controller.ts`
|
||||
```typescript
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { loadFeatureConfig } from '../config/feature-loader';
|
||||
|
||||
@Controller('features')
|
||||
export class FeaturesController {
|
||||
@Get()
|
||||
async getFeatures() {
|
||||
const result = await loadFeatureConfig();
|
||||
return {
|
||||
features: result.features,
|
||||
loadedFrom: result.loadedFrom,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**File**: `apps/api/src/features/features.module.ts`
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FeaturesController } from './features.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [FeaturesController],
|
||||
})
|
||||
export class FeaturesModule {}
|
||||
```
|
||||
|
||||
**Update**: `apps/api/src/app.module.ts`
|
||||
```typescript
|
||||
import { FeaturesModule } from './features/features.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing modules
|
||||
FeaturesModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### Step 2: Update Website FeatureFlagService
|
||||
|
||||
**Replace**: `apps/website/lib/feature/FeatureFlagService.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* FeatureFlagService - Reads features from API
|
||||
*
|
||||
* Single Source of Truth: API /api/features endpoint
|
||||
*/
|
||||
|
||||
export class FeatureFlagService {
|
||||
private flags: Set<string>;
|
||||
|
||||
constructor(flags: string[]) {
|
||||
this.flags = new Set(flags);
|
||||
}
|
||||
|
||||
isEnabled(flag: string): boolean {
|
||||
return this.flags.has(flag);
|
||||
}
|
||||
|
||||
getEnabledFlags(): string[] {
|
||||
return Array.from(this.flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load features from API - Single Source of Truth
|
||||
*/
|
||||
static async fromAPI(): Promise<FeatureFlagService> {
|
||||
try {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||
const response = await fetch(`${baseUrl}/features`, {
|
||||
cache: 'no-store', // Always get fresh data
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const flags = Object.keys(data.features).filter(
|
||||
key => data.features[key] === 'enabled'
|
||||
);
|
||||
|
||||
return new FeatureFlagService(flags);
|
||||
} catch (error) {
|
||||
console.error('Failed to load features from API:', error);
|
||||
// Fallback: empty service (secure by default)
|
||||
return new FeatureFlagService([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock for testing/local development
|
||||
*/
|
||||
static mock(flags: string[] = []): FeatureFlagService {
|
||||
return new FeatureFlagService(flags);
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side context interface
|
||||
export interface FeatureFlagContextType {
|
||||
isEnabled: (flag: string) => boolean;
|
||||
getEnabledFlags: () => string[];
|
||||
}
|
||||
|
||||
// Default mock for client-side when API unavailable
|
||||
export const mockFeatureFlags = new FeatureFlagService([
|
||||
'driver_profiles',
|
||||
'team_profiles',
|
||||
'wallets',
|
||||
'sponsors',
|
||||
'team_feature',
|
||||
'alpha_features'
|
||||
]);
|
||||
```
|
||||
|
||||
### Step 3: Update Website to Use API Features
|
||||
|
||||
**Update**: `apps/website/lib/services/home/getHomeData.ts`
|
||||
```typescript
|
||||
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
|
||||
export async function getHomeData() {
|
||||
const container = ContainerManager.getInstance().getContainer();
|
||||
const sessionService = container.get<SessionService>(SESSION_SERVICE_TOKEN);
|
||||
const landingService = container.get<LandingService>(LANDING_SERVICE_TOKEN);
|
||||
|
||||
const session = await sessionService.getSession();
|
||||
if (session) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
|
||||
// Load features from API (single source of truth)
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const isAlpha = featureService.isEnabled('alpha_features');
|
||||
const discovery = await landingService.getHomeDiscovery();
|
||||
|
||||
return {
|
||||
isAlpha,
|
||||
upcomingRaces: discovery.upcomingRaces,
|
||||
topLeagues: discovery.topLeagues,
|
||||
teams: discovery.teams,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Update**: `apps/website/app/layout.tsx`
|
||||
```typescript
|
||||
// Remove: import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
|
||||
// Remove: const featureService = FeatureFlagService.fromEnv();
|
||||
// Remove: const enabledFlags = featureService.getEnabledFlags();
|
||||
|
||||
// Keep only: <FeatureFlagProvider flags={enabledFlags}>
|
||||
|
||||
// Instead, wrap the app to load features from API
|
||||
export default async function RootLayout({ children }) {
|
||||
// Load features from API
|
||||
const featureService = await FeatureFlagService.fromAPI();
|
||||
const enabledFlags = featureService.getEnabledFlags();
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<FeatureFlagProvider flags={enabledFlags}>
|
||||
{children}
|
||||
</FeatureFlagProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Remove Website-Specific Config
|
||||
|
||||
**Delete**: `apps/website/lib/mode.ts` (no longer needed)
|
||||
**Delete**: `apps/website/lib/feature/FeatureFlagService.ts` (replaced)
|
||||
**Delete**: `apps/website/lib/feature/FeatureFlagService.test.ts` (replaced)
|
||||
|
||||
**Update**: `apps/website/lib/config/env.ts`
|
||||
```typescript
|
||||
// Remove NEXT_PUBLIC_GRIDPILOT_MODE from schema
|
||||
const publicEnvSchema = z.object({
|
||||
// NEXT_PUBLIC_GRIDPILOT_MODE: z.enum(['pre-launch', 'alpha']).optional(), // REMOVE
|
||||
NEXT_PUBLIC_SITE_URL: urlOptional,
|
||||
NEXT_PUBLIC_API_BASE_URL: urlOptional,
|
||||
// ... other env vars
|
||||
});
|
||||
```
|
||||
|
||||
### Step 5: Update Environment Files
|
||||
|
||||
**Update**: `.env.development`
|
||||
```bash
|
||||
# Remove:
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
|
||||
# Keep:
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
**Update**: `.env.production`
|
||||
```bash
|
||||
# Remove:
|
||||
# NEXT_PUBLIC_GRIDPILOT_MODE=beta
|
||||
|
||||
# Keep:
|
||||
NEXT_PUBLIC_API_BASE_URL=https://api.gridpilot.com
|
||||
```
|
||||
|
||||
### Step 6: Update Tests
|
||||
|
||||
**Update**: `apps/api/src/config/integration.test.ts`
|
||||
```typescript
|
||||
// Add test for feature endpoint
|
||||
describe('Features API Endpoint', () => {
|
||||
it('should return all features from API', async () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
// Verify API returns all expected features
|
||||
expect(result.features['platform.dashboard']).toBe('enabled');
|
||||
expect(result.features['alpha_features']).toBe('enabled');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Update**: `apps/website/lib/feature/FeatureFlagService.test.ts`
|
||||
```typescript
|
||||
// Test API loading
|
||||
describe('FeatureFlagService.fromAPI()', () => {
|
||||
it('should load features from API endpoint', async () => {
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
features: {
|
||||
'platform.dashboard': 'enabled',
|
||||
'alpha_features': 'enabled',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const service = await FeatureFlagService.fromAPI();
|
||||
expect(service.isEnabled('platform.dashboard')).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Step 7: Update Documentation
|
||||
|
||||
**Update**: `docs/FEATURE_ARCHITECTURE.md`
|
||||
```markdown
|
||||
# Feature Architecture - API-Driven
|
||||
|
||||
## Single Source of Truth
|
||||
|
||||
**Location**: `apps/api/src/config/features.config.ts`
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **API Config** defines all features and their states
|
||||
2. **API Endpoint** `/features` exposes current configuration
|
||||
3. **Website** loads features from API at runtime
|
||||
4. **PolicyService** uses API config for security enforcement
|
||||
|
||||
## Benefits
|
||||
|
||||
- ✅ One file to maintain
|
||||
- ✅ Automatic synchronization
|
||||
- ✅ Runtime feature changes
|
||||
- ✅ Security-first (API controls everything)
|
||||
- ✅ No duplication
|
||||
|
||||
## Feature States (Same as before)
|
||||
|
||||
- `enabled` = Fully available
|
||||
- `disabled` = Completely blocked
|
||||
- `coming_soon` = Visible but not functional
|
||||
- `hidden` = Invisible
|
||||
|
||||
## Environment Mapping
|
||||
|
||||
| Environment | API Config | Website Behavior |
|
||||
|-------------|------------|------------------|
|
||||
| Development | All enabled | Full platform access |
|
||||
| Test | All enabled | Full testing |
|
||||
| Staging | Controlled | Beta features visible |
|
||||
| Production | Stable only | Production features only |
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] Create API endpoint
|
||||
- [ ] Update FeatureFlagService to use API
|
||||
- [ ] Update website components to use new service
|
||||
- [ ] Remove old website config files
|
||||
- [ ] Update environment files
|
||||
- [ ] Update tests
|
||||
- [ ] Update documentation
|
||||
- [ ] Test end-to-end
|
||||
- [ ] Verify security (no leaks)
|
||||
|
||||
## Expected Results
|
||||
|
||||
**Before**: Two configs, risk of mismatch, manual sync
|
||||
**After**: One config, automatic sync, secure by default
|
||||
|
||||
**Files Changed**: ~8 files
|
||||
**Files Deleted**: ~3 files
|
||||
**Time Estimate**: 2-3 hours
|
||||
Reference in New Issue
Block a user