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

6.0 KiB

Feature Architecture: API-Driven Feature Flags

The Problem

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 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: API-Driven Features

Single Source of Truth: API

All feature control now comes from the API /features endpoint:

// API Response (from /features endpoint)
{
  "alpha_features": true,
  "platform.dashboard": true,
  "platform.leagues": true,
  "platform.teams": true,
  "sponsors.portal": true,
  // ... all other features
}

FeatureFlagService - The Bridge

The FeatureFlagService is the only way to check feature availability:

// ✅ CORRECT: Use FeatureFlagService
const service = await FeatureFlagService.fromAPI();
const isEnabled = service.isEnabled('platform.dashboard');

// ❌ WRONG: No more environment mode checks
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; // REMOVED

How It Works

  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: Always Use FeatureFlagService

// ✅ 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 />;
}

Rule 2: Use ModeGuard for Conditional Rendering

// ✅ 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: Use Hooks for Client Components

// ✅ CORRECT: Use hooks in client components
import { useFeature, useFeatures } from '@/components/shared/ModeGuard';

function ClientComponent() {
  const hasDashboard = useFeature('platform.dashboard');
  const features = useFeatures(['platform.dashboard', 'platform.leagues']);
  
  if (!hasDashboard) return null;
  
  return <div>Dashboard Content</div>;
}

Configuration Examples

API Configuration (features.config.ts)

// apps/api/src/config/features.config.ts
export const featuresConfig = {
  development: {
    // Alpha features enabled for development
    alpha_features: 'enabled',
    platform: {
      dashboard: 'enabled',
      leagues: 'enabled',
      teams: 'enabled',
      races: 'enabled',
    },
    sponsors: {
      portal: 'enabled',
    },
  },
  
  production: {
    // Gradual rollout in production
    alpha_features: 'disabled',
    platform: {
      dashboard: 'enabled',
      leagues: 'enabled',
      teams: 'enabled',
      races: 'enabled',
    },
    sponsors: {
      portal: 'coming_soon', // Not ready yet
    },
  },
};

No More Environment Variables

# ❌ REMOVED
NEXT_PUBLIC_GRIDPILOT_MODE=alpha

# ✅ ONLY NEEDED
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001

Migration Path

Before (Confusing)

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

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

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:

# 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.