Files
gridpilot.gg/plans/API_DRIVEN_FEATURE_CONFIG.md
2026-01-07 22:05:53 +01:00

8.7 KiB

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

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

import { Module } from '@nestjs/common';
import { FeaturesController } from './features.controller';

@Module({
  controllers: [FeaturesController],
})
export class FeaturesModule {}

Update: apps/api/src/app.module.ts

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

/**
 * 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

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

// 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

// 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

# Remove:
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha

# Keep:
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001

Update: .env.production

# 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

// 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

// 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

# 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