feature flags

This commit is contained in:
2026-01-03 12:07:20 +01:00
parent 9a7efa496f
commit 213580511c
5 changed files with 289 additions and 26 deletions

View File

@@ -1,12 +1,15 @@
# GridPilot Website Environment Variables
# Application Mode
# Controls whether the site is in pre-launch or alpha mode
# Valid values: "pre-launch" | "alpha"
# Default: "pre-launch" (if not set)
# Note: NEXT_PUBLIC_ prefix exposes this to both server and browser
# pre-launch = landing page only, no features
# alpha = full platform with all features enabled automatically
NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# Feature Flags (Optional - only needed for custom feature selection)
# When in alpha mode, all features are enabled automatically
# Use this to override or select specific features
# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature
# Vercel KV (for email signups and rate limiting)
# Get these from: https://vercel.com/dashboard -> Storage -> KV
# OPTIONAL in development (uses in-memory fallback)
@@ -23,5 +26,14 @@ NEXT_PUBLIC_SITE_URL=https://gridpilot.com
# Example: https://discord.gg/your-invite-code
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
# Example for alpha mode:
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# Example configurations:
# Pre-launch (default)
# NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# Alpha with all features (recommended)
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# Alpha with specific features only
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# FEATURE_FLAGS=driver_profiles,wallets

View File

@@ -58,7 +58,13 @@ Visit `http://localhost:3000` to see the landing page.
| Variable | Description | Example |
|----------|-------------|---------|
| `NEXT_PUBLIC_GRIDPILOT_MODE` | Application mode (server & client) | `pre-launch` or `alpha` |
| `NEXT_PUBLIC_GRIDPILOT_MODE` | Application mode (pre-launch or full platform) | `pre-launch` or `alpha` |
### Optional Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `FEATURE_FLAGS` | Feature override (comma-separated) | `driver_profiles,wallets` |
| `KV_REST_API_URL` | Vercel KV REST API endpoint | From Vercel Dashboard |
| `KV_REST_API_TOKEN` | Vercel KV authentication token | From Vercel Dashboard |
| `NEXT_PUBLIC_SITE_URL` | Public site URL | `https://gridpilot.com` |
@@ -71,31 +77,95 @@ Visit `http://localhost:3000` to see the landing page.
4. Copy the `KV_REST_API_URL` and `KV_REST_API_TOKEN` from the database settings
5. Add them to your environment variables
## Mode Switching
The application supports two modes:
## Application Modes
### Pre-Launch Mode (Default)
- Shows landing page with email capture
- Only `/` and `/api/signup` routes are accessible
- All other routes return 404
To activate:
```bash
NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
```
- Landing page only
- Email signup functionality
- No navigation or footer
- All platform features disabled
### Post-Launch Mode
- Full platform access
- All routes are accessible
- Landing page is still available at `/`
To activate:
### Alpha Mode (Full Platform)
```bash
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
```
- Full platform access
- **All features automatically enabled**
- Navigation and footer visible
- Discovery section on landing page
### Available Feature Flags
When in alpha mode, these features are automatically enabled:
- `driver_profiles` - Driver profile pages and features
- `team_profiles` - Team profile pages and features
- `wallets` - All wallet functionality (driver/team/league/sponsor)
- `sponsors` - Sponsor management features
- `team_feature` - Complete team functionality
- `alpha_features` - Landing page discovery section
### Custom Feature Selection (Optional)
If you need fine-grained control, use `FEATURE_FLAGS`:
```bash
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
FEATURE_FLAGS=driver_profiles,wallets
```
## Configuration Examples
### Basic Alpha Setup (Recommended)
```bash
# .env.local
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# All features enabled automatically - no FEATURE_FLAGS needed
```
### Pre-Launch Setup
```bash
# .env.local
NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# Landing page only
```
### Custom Feature Selection
```bash
# .env.local
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
FEATURE_FLAGS=driver_profiles,wallets
# Only driver profiles and wallets enabled
```
## How It Works
1. **Mode Detection**: `NEXT_PUBLIC_GRIDPILOT_MODE` sets overall platform access
2. **Feature Bundling**: Alpha mode automatically enables all features via `FeatureFlagService`
3. **Override**: `FEATURE_FLAGS` can selectively enable/disable features
4. **Component Usage**: Components check feature flags using `FeatureFlagService.fromEnv().isEnabled('feature_name')`
## Migration Guide
### From Manual Feature Flags
**Before**:
```bash
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
FEATURE_FLAGS=alpha_features,driver_profiles,team_profiles,wallets,sponsors,team_feature
```
**After**:
```bash
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# FEATURE_FLAGS not needed - everything automatic!
```
### No Code Changes Required
Your existing code continues to work:
```typescript
const featureService = FeatureFlagService.fromEnv();
const isAlpha = featureService.isEnabled('alpha_features');
// Works in both old and new system
```
## Email Signup API

View File

@@ -66,6 +66,7 @@ declare global {
// Website (server-only)
API_BASE_URL?: string;
FEATURE_FLAGS?: string;
// Vercel KV (server-only)
KV_REST_API_URL?: string;

View File

@@ -0,0 +1,144 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService';
describe('FeatureFlagService', () => {
describe('fromEnv() with alpha mode integration', () => {
let originalMode: string | undefined;
let originalFlags: string | undefined;
beforeEach(() => {
originalMode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
originalFlags = process.env.FEATURE_FLAGS;
});
afterEach(() => {
if (originalMode !== undefined) {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = originalMode;
} else {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
}
if (originalFlags !== undefined) {
process.env.FEATURE_FLAGS = originalFlags;
} else {
delete process.env.FEATURE_FLAGS;
}
});
it('should enable all features when NEXT_PUBLIC_GRIDPILOT_MODE is alpha', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(true);
expect(service.isEnabled('team_profiles')).toBe(true);
expect(service.isEnabled('wallets')).toBe(true);
expect(service.isEnabled('sponsors')).toBe(true);
expect(service.isEnabled('team_feature')).toBe(true);
expect(service.isEnabled('alpha_features')).toBe(true);
});
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is pre-launch', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(false);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('wallets')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false);
expect(service.isEnabled('alpha_features')).toBe(false);
});
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(false);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('wallets')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false);
expect(service.isEnabled('alpha_features')).toBe(false);
});
it('should allow FEATURE_FLAGS to override alpha mode', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
process.env.FEATURE_FLAGS = 'driver_profiles,wallets';
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(true);
expect(service.isEnabled('wallets')).toBe(true);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false);
});
it('should return correct list of enabled flags in alpha mode', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
const service = FeatureFlagService.fromEnv();
const enabledFlags = service.getEnabledFlags();
expect(enabledFlags).toContain('driver_profiles');
expect(enabledFlags).toContain('team_profiles');
expect(enabledFlags).toContain('wallets');
expect(enabledFlags).toContain('sponsors');
expect(enabledFlags).toContain('team_feature');
expect(enabledFlags).toContain('alpha_features');
expect(enabledFlags.length).toBe(6);
});
});
describe('Constructor behavior', () => {
it('should use provided flags array', () => {
const service = new FeatureFlagService(['test_flag']);
expect(service.isEnabled('test_flag')).toBe(true);
expect(service.isEnabled('other_flag')).toBe(false);
});
it('should parse FEATURE_FLAGS environment variable', () => {
process.env.FEATURE_FLAGS = 'flag1, flag2, flag3';
const service = new FeatureFlagService();
expect(service.isEnabled('flag1')).toBe(true);
expect(service.isEnabled('flag2')).toBe(true);
expect(service.isEnabled('flag3')).toBe(true);
expect(service.isEnabled('flag4')).toBe(false);
delete process.env.FEATURE_FLAGS;
});
it('should handle empty FEATURE_FLAGS', () => {
process.env.FEATURE_FLAGS = '';
const service = new FeatureFlagService();
expect(service.isEnabled('any_flag')).toBe(false);
expect(service.getEnabledFlags()).toEqual([]);
delete process.env.FEATURE_FLAGS;
});
});
});
describe('MockFeatureFlagService', () => {
it('should work with provided flags', () => {
const service = new MockFeatureFlagService(['test_flag']);
expect(service.isEnabled('test_flag')).toBe(true);
expect(service.isEnabled('other_flag')).toBe(false);
});
it('should return empty array when no flags provided', () => {
const service = new MockFeatureFlagService();
expect(service.getEnabledFlags()).toEqual([]);
});
});
describe('mockFeatureFlags default instance', () => {
it('should have alpha_features enabled by default', () => {
expect(mockFeatureFlags.isEnabled('alpha_features')).toBe(true);
});
});

View File

@@ -1,7 +1,12 @@
/**
* FeatureFlagService - Manages feature flags for both server and client
*
*
* Automatic Alpha Mode Integration:
* When NEXT_PUBLIC_GRIDPILOT_MODE=alpha, all features are automatically enabled.
* This eliminates the need to manually set FEATURE_FLAGS for alpha deployments.
*
* Server: Reads from process.env.FEATURE_FLAGS (comma-separated)
* OR auto-enables all features if in alpha mode
* Client: Reads from session context or provides mock implementation
*/
@@ -15,7 +20,7 @@ export class FeatureFlagService {
} else {
// Parse from environment variable
const flagsEnv = process.env.FEATURE_FLAGS;
this.flags = flagsEnv
this.flags = flagsEnv
? new Set(flagsEnv.split(',').map(f => f.trim()))
: new Set();
}
@@ -37,8 +42,31 @@ export class FeatureFlagService {
/**
* Factory method to create service with environment flags
* Automatically enables all features if in alpha mode
* FEATURE_FLAGS can override alpha mode defaults
*/
static fromEnv(): FeatureFlagService {
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
const flagsEnv = process.env.FEATURE_FLAGS;
// If FEATURE_FLAGS is explicitly set, use it (overrides alpha mode)
if (flagsEnv) {
return new FeatureFlagService();
}
// If in alpha mode, automatically enable all features
if (mode === 'alpha') {
return new FeatureFlagService([
'driver_profiles',
'team_profiles',
'wallets',
'sponsors',
'team_feature',
'alpha_features'
]);
}
// Otherwise, use FEATURE_FLAGS environment variable (empty if not set)
return new FeatureFlagService();
}
}
@@ -67,4 +95,12 @@ export class MockFeatureFlagService implements FeatureFlagContextType {
}
// Default mock instance for client-side usage
export const mockFeatureFlags = new MockFeatureFlagService(['alpha_features']);
// Enables all features for development/demo mode
export const mockFeatureFlags = new MockFeatureFlagService([
'driver_profiles',
'team_profiles',
'wallets',
'sponsors',
'team_feature',
'alpha_features'
]);