# 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; 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 { 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(SESSION_SERVICE_TOKEN); const landingService = container.get(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: // 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 ( {children} ); } ``` ### 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