# ADR-024 Implementation Plan: Feature Flagging Strategy **Date**: 2026-01-28 **Type**: Technical Implementation Plan **Related**: [ADR-024: Feature Flagging Strategy](../adr/0024-feature-flagging-strategy.md), [ADR-007: Configuration and Secrets Management](../adr/0007-configuration-and-secrets-management.md) **Status**: Ready for Implementation --- ## Project Overview Implement a simple, configuration-based feature flag system that integrates with the existing Zod-validated configuration in `src/config/env.ts`. The system will support both backend and frontend feature flags through environment variables, with type-safe access patterns and helper utilities. ### Key Success Criteria 1. Feature flags accessible via type-safe API on both backend and frontend 2. Zero runtime overhead when flag is disabled (compile-time elimination where possible) 3. Consistent naming convention (environment variables and code access) 4. Graceful degradation (missing flag defaults to disabled) 5. Easy migration path to external service (Flagsmith/LaunchDarkly) in the future 6. Full test coverage with mocking utilities ### Estimated Total Effort | Phase | Estimate | | --------------------------------- | -------------- | | Phase 1: Backend Infrastructure | 3-5 hours | | Phase 2: Frontend Infrastructure | 2-3 hours | | Phase 3: Documentation & Examples | 1-2 hours | | **Total** | **6-10 hours** | --- ## Current State Analysis ### Backend Configuration (`src/config/env.ts`) - Zod-based schema validation at startup - Organized into logical groups (database, redis, auth, smtp, ai, etc.) - Helper exports for service availability (`isSmtpConfigured`, `isAiConfigured`, etc.) - Environment helpers (`isProduction`, `isTest`, `isDevelopment`) - Fail-fast on invalid configuration ### Frontend Configuration (`src/config.ts`) - Uses `import.meta.env` (Vite environment variables) - Organized into sections (app, google, sentry) - Boolean parsing for string env vars - Type declarations in `src/vite-env.d.ts` ### Existing Patterns to Follow ```typescript // Backend - service availability check pattern export const isSmtpConfigured = !!config.smtp.host && !!config.smtp.user && !!config.smtp.pass; // Frontend - boolean parsing pattern enabled: import.meta.env.VITE_SENTRY_ENABLED !== 'false', ``` --- ## Task Breakdown ### Phase 1: Backend Feature Flag Infrastructure #### [1.1] Define Feature Flag Schema in env.ts **Complexity**: Low **Estimate**: 30-45 minutes **Dependencies**: None **Parallelizable**: Yes **Description**: Add a new `featureFlags` section to the Zod schema in `src/config/env.ts`. **Acceptance Criteria**: - [ ] New `featureFlagsSchema` Zod object defined - [ ] Schema supports boolean flags with defaults to `false` (opt-in model) - [ ] Schema added to main `envSchema` object - [ ] Type exported as part of `EnvConfig` **Implementation Details**: ```typescript // src/config/env.ts /** * Feature flags configuration schema (ADR-024). * All flags default to false (disabled) for safety. * Set to 'true' in environment to enable. */ const featureFlagsSchema = z.object({ // Example flags - replace with actual feature flags as needed newDashboard: booleanString(false), // FEATURE_NEW_DASHBOARD betaRecipes: booleanString(false), // FEATURE_BETA_RECIPES experimentalAi: booleanString(false), // FEATURE_EXPERIMENTAL_AI debugMode: booleanString(false), // FEATURE_DEBUG_MODE }); // In loadEnvVars(): featureFlags: { newDashboard: process.env.FEATURE_NEW_DASHBOARD, betaRecipes: process.env.FEATURE_BETA_RECIPES, experimentalAi: process.env.FEATURE_EXPERIMENTAL_AI, debugMode: process.env.FEATURE_DEBUG_MODE, }, ``` **Risks/Notes**: - Naming convention: `FEATURE_*` prefix for all feature flag env vars - Default to `false` ensures features are opt-in, preventing accidental exposure --- #### [1.2] Create Feature Flag Service Module **Complexity**: Medium **Estimate**: 1-2 hours **Dependencies**: [1.1] **Parallelizable**: No (depends on 1.1) **Description**: Create a dedicated service module for feature flag access with helper functions. **File**: `src/services/featureFlags.server.ts` **Acceptance Criteria**: - [ ] `isFeatureEnabled(flagName)` function for checking flags - [ ] `getAllFeatureFlags()` function for debugging/admin endpoints - [ ] Type-safe flag name parameter (union type or enum) - [ ] Exported helper booleans for common flags (similar to `isSmtpConfigured`) - [ ] Logging when feature flag is checked in development mode **Implementation Details**: ```typescript // src/services/featureFlags.server.ts import { config, isDevelopment } from '../config/env'; import { logger } from './logger.server'; export type FeatureFlagName = keyof typeof config.featureFlags; /** * Check if a feature flag is enabled. * @param flagName - The name of the feature flag to check * @returns boolean indicating if the feature is enabled */ export function isFeatureEnabled(flagName: FeatureFlagName): boolean { const enabled = config.featureFlags[flagName]; if (isDevelopment) { logger.debug({ flag: flagName, enabled }, 'Feature flag checked'); } return enabled; } /** * Get all feature flags and their current states. * Useful for debugging and admin endpoints. */ export function getAllFeatureFlags(): Record { return { ...config.featureFlags }; } // Convenience exports for common flag checks export const isNewDashboardEnabled = config.featureFlags.newDashboard; export const isBetaRecipesEnabled = config.featureFlags.betaRecipes; export const isExperimentalAiEnabled = config.featureFlags.experimentalAi; export const isDebugModeEnabled = config.featureFlags.debugMode; ``` **Risks/Notes**: - Keep logging minimal to avoid performance impact - Convenience exports are evaluated once at startup (not dynamic) --- #### [1.3] Add Admin Endpoint for Feature Flag Status **Complexity**: Low **Estimate**: 30-45 minutes **Dependencies**: [1.2] **Parallelizable**: No (depends on 1.2) **Description**: Add an admin/health endpoint to view current feature flag states. **File**: `src/routes/admin.routes.ts` (or `stats.routes.ts` if admin routes don't exist) **Acceptance Criteria**: - [ ] `GET /api/v1/admin/feature-flags` endpoint (admin-only) - [ ] Returns JSON object with all flags and their states - [ ] Requires admin authentication - [ ] Endpoint documented in Swagger **Implementation Details**: ```typescript // In appropriate routes file router.get('/feature-flags', requireAdmin, async (req, res) => { const flags = getAllFeatureFlags(); sendSuccess(res, { flags }); }); ``` **Risks/Notes**: - Ensure endpoint is protected (admin-only) - Consider caching response if called frequently --- #### [1.4] Backend Unit Tests **Complexity**: Medium **Estimate**: 1-2 hours **Dependencies**: [1.1], [1.2] **Parallelizable**: Yes (can start after 1.1, in parallel with 1.3) **Description**: Write unit tests for feature flag configuration and service. **Files**: - `src/config/env.test.ts` (add feature flag tests) - `src/services/featureFlags.server.test.ts` (new file) **Acceptance Criteria**: - [ ] Test default values (all false) - [ ] Test parsing 'true'/'false' strings - [ ] Test `isFeatureEnabled()` function - [ ] Test `getAllFeatureFlags()` function - [ ] Test type safety (TypeScript compile-time checks) **Implementation Details**: ```typescript // src/config/env.test.ts - add to existing file describe('featureFlags configuration', () => { it('should default all feature flags to false', async () => { setValidEnv(); const { config } = await import('./env'); expect(config.featureFlags.newDashboard).toBe(false); expect(config.featureFlags.betaRecipes).toBe(false); }); it('should parse FEATURE_NEW_DASHBOARD as true when set', async () => { setValidEnv({ FEATURE_NEW_DASHBOARD: 'true' }); const { config } = await import('./env'); expect(config.featureFlags.newDashboard).toBe(true); }); }); // src/services/featureFlags.server.test.ts - new file describe('featureFlags service', () => { describe('isFeatureEnabled', () => { it('should return false for disabled flags', () => { expect(isFeatureEnabled('newDashboard')).toBe(false); }); // ... more tests }); }); ``` --- ### Phase 2: Frontend Feature Flag Infrastructure #### [2.1] Add Frontend Feature Flag Config **Complexity**: Low **Estimate**: 30-45 minutes **Dependencies**: None (can run in parallel with Phase 1) **Parallelizable**: Yes **Description**: Add feature flags to the frontend config module. **Files**: - `src/config.ts` - Add featureFlags section - `src/vite-env.d.ts` - Add type declarations **Acceptance Criteria**: - [ ] Feature flags section added to `src/config.ts` - [ ] TypeScript declarations updated in `vite-env.d.ts` - [ ] Boolean parsing consistent with existing pattern - [ ] Default to false when env var not set **Implementation Details**: ```typescript // src/config.ts const config = { // ... existing sections ... /** * Feature flags for conditional feature rendering (ADR-024). * All flags default to false (disabled) when not explicitly set. */ featureFlags: { newDashboard: import.meta.env.VITE_FEATURE_NEW_DASHBOARD === 'true', betaRecipes: import.meta.env.VITE_FEATURE_BETA_RECIPES === 'true', experimentalAi: import.meta.env.VITE_FEATURE_EXPERIMENTAL_AI === 'true', debugMode: import.meta.env.VITE_FEATURE_DEBUG_MODE === 'true', }, }; // src/vite-env.d.ts interface ImportMetaEnv { // ... existing declarations ... readonly VITE_FEATURE_NEW_DASHBOARD?: string; readonly VITE_FEATURE_BETA_RECIPES?: string; readonly VITE_FEATURE_EXPERIMENTAL_AI?: string; readonly VITE_FEATURE_DEBUG_MODE?: string; } ``` --- #### [2.2] Create useFeatureFlag React Hook **Complexity**: Medium **Estimate**: 1-1.5 hours **Dependencies**: [2.1] **Parallelizable**: No (depends on 2.1) **Description**: Create a React hook for checking feature flags in components. **File**: `src/hooks/useFeatureFlag.ts` **Acceptance Criteria**: - [ ] `useFeatureFlag(flagName)` hook returns boolean - [ ] Type-safe flag name parameter - [ ] Memoized to prevent unnecessary re-renders - [ ] Optional `FeatureFlag` component for conditional rendering **Implementation Details**: ```typescript // src/hooks/useFeatureFlag.ts import { useMemo } from 'react'; import config from '../config'; export type FeatureFlagName = keyof typeof config.featureFlags; /** * Hook to check if a feature flag is enabled. * * @param flagName - The name of the feature flag to check * @returns boolean indicating if the feature is enabled * * @example * const isNewDashboard = useFeatureFlag('newDashboard'); * if (isNewDashboard) { * return ; * } */ export function useFeatureFlag(flagName: FeatureFlagName): boolean { return useMemo(() => config.featureFlags[flagName], [flagName]); } /** * Get all feature flags (useful for debugging). */ export function useAllFeatureFlags(): Record { return useMemo(() => ({ ...config.featureFlags }), []); } ``` --- #### [2.3] Create FeatureFlag Component **Complexity**: Low **Estimate**: 30-45 minutes **Dependencies**: [2.2] **Parallelizable**: No (depends on 2.2) **Description**: Create a declarative component for feature flag conditional rendering. **File**: `src/components/FeatureFlag.tsx` **Acceptance Criteria**: - [ ] `` component - [ ] Children rendered only when flag is enabled - [ ] Optional `fallback` prop for disabled state - [ ] TypeScript-enforced flag names **Implementation Details**: ```typescript // src/components/FeatureFlag.tsx import { ReactNode } from 'react'; import { useFeatureFlag, FeatureFlagName } from '../hooks/useFeatureFlag'; interface FeatureFlagProps { /** The name of the feature flag to check */ name: FeatureFlagName; /** Content to render when feature is enabled */ children: ReactNode; /** Optional content to render when feature is disabled */ fallback?: ReactNode; } /** * Conditionally renders children based on feature flag state. * * @example * }> * * */ export function FeatureFlag({ name, children, fallback = null }: FeatureFlagProps) { const isEnabled = useFeatureFlag(name); return <>{isEnabled ? children : fallback}; } ``` --- #### [2.4] Frontend Unit Tests **Complexity**: Medium **Estimate**: 1-1.5 hours **Dependencies**: [2.1], [2.2], [2.3] **Parallelizable**: No (depends on previous frontend tasks) **Description**: Write unit tests for frontend feature flag utilities. **Files**: - `src/config.test.ts` (add feature flag tests) - `src/hooks/useFeatureFlag.test.ts` (new file) - `src/components/FeatureFlag.test.tsx` (new file) **Acceptance Criteria**: - [ ] Test config structure includes featureFlags - [ ] Test default values (all false) - [ ] Test hook returns correct values - [ ] Test component renders/hides children correctly - [ ] Test fallback rendering **Implementation Details**: ```typescript // src/hooks/useFeatureFlag.test.ts import { renderHook } from '@testing-library/react'; import { useFeatureFlag, useAllFeatureFlags } from './useFeatureFlag'; describe('useFeatureFlag', () => { it('should return false for disabled flags', () => { const { result } = renderHook(() => useFeatureFlag('newDashboard')); expect(result.current).toBe(false); }); }); // src/components/FeatureFlag.test.tsx import { render, screen } from '@testing-library/react'; import { FeatureFlag } from './FeatureFlag'; describe('FeatureFlag', () => { it('should not render children when flag is disabled', () => { render(
New Feature
); expect(screen.queryByTestId('new-feature')).not.toBeInTheDocument(); }); it('should render fallback when flag is disabled', () => { render( Old Feature}>
New Feature
); expect(screen.getByText('Old Feature')).toBeInTheDocument(); }); }); ``` --- ### Phase 3: Documentation & Integration #### [3.1] Update ADR-024 with Implementation Status **Complexity**: Low **Estimate**: 30 minutes **Dependencies**: [1.1], [1.2], [2.1], [2.2] **Parallelizable**: Yes (can be done after core implementation) **Description**: Update ADR-024 to mark it as implemented and add implementation details. **File**: `docs/adr/0024-feature-flagging-strategy.md` **Acceptance Criteria**: - [ ] Status changed from "Proposed" to "Accepted" - [ ] Implementation status section added - [ ] Key files documented - [ ] Usage examples included --- #### [3.2] Update Environment Documentation **Complexity**: Low **Estimate**: 30 minutes **Dependencies**: [1.1], [2.1] **Parallelizable**: Yes **Description**: Add feature flag environment variables to documentation. **Files**: - `docs/getting-started/ENVIRONMENT.md` - `.env.example` **Acceptance Criteria**: - [ ] Feature flag variables documented in ENVIRONMENT.md - [ ] New section "Feature Flags" added - [ ] `.env.example` updated with commented feature flag examples **Implementation Details**: ```bash # .env.example addition # =================== # Feature Flags (ADR-024) # =================== # All feature flags default to disabled (false) when not set. # Set to 'true' to enable a feature. # # FEATURE_NEW_DASHBOARD=false # FEATURE_BETA_RECIPES=false # FEATURE_EXPERIMENTAL_AI=false # FEATURE_DEBUG_MODE=false # # Frontend equivalents (prefix with VITE_): # VITE_FEATURE_NEW_DASHBOARD=false # VITE_FEATURE_BETA_RECIPES=false ``` --- #### [3.3] Create CODE-PATTERNS Entry **Complexity**: Low **Estimate**: 30 minutes **Dependencies**: All implementation tasks **Parallelizable**: Yes **Description**: Add feature flag usage patterns to CODE-PATTERNS.md. **File**: `docs/development/CODE-PATTERNS.md` **Acceptance Criteria**: - [ ] Feature flag section added with examples - [ ] Backend usage pattern documented - [ ] Frontend usage pattern documented - [ ] Testing pattern documented --- #### [3.4] Update CLAUDE.md Quick Reference **Complexity**: Low **Estimate**: 15 minutes **Dependencies**: All implementation tasks **Parallelizable**: Yes **Description**: Add feature flags to the CLAUDE.md quick reference tables. **File**: `CLAUDE.md` **Acceptance Criteria**: - [ ] Feature flags added to "Key Patterns" table - [ ] Reference to featureFlags service added --- ## Implementation Sequence ### Phase 1 (Backend) - Can Start Immediately ```text [1.1] Schema ──────────┬──> [1.2] Service ──> [1.3] Admin Endpoint │ └──> [1.4] Backend Tests (can start after 1.1) ``` ### Phase 2 (Frontend) - Can Start Immediately (Parallel with Phase 1) ```text [2.1] Config ──> [2.2] Hook ──> [2.3] Component ──> [2.4] Frontend Tests ``` ### Phase 3 (Documentation) - After Implementation ```text All Phase 1 & 2 Tasks ──> [3.1] ADR Update ├──> [3.2] Env Docs ├──> [3.3] Code Patterns └──> [3.4] CLAUDE.md ``` --- ## Critical Path The minimum path to a working feature flag system: 1. **[1.1] Schema** (30 min) - Required for backend 2. **[1.2] Service** (1.5 hr) - Required for backend access 3. **[2.1] Frontend Config** (30 min) - Required for frontend 4. **[2.2] Hook** (1 hr) - Required for React integration **Critical path duration**: ~3.5 hours Non-critical but recommended: - Admin endpoint (debugging) - FeatureFlag component (developer convenience) - Tests (quality assurance) - Documentation (maintainability) --- ## Scope Recommendations ### MVP (Minimum Viable Implementation) Include in initial implementation: - [1.1] Backend schema with 2-3 example flags - [1.2] Feature flag service - [2.1] Frontend config - [2.2] useFeatureFlag hook - [1.4] Core backend tests - [2.4] Core frontend tests ### Enhancements (Future Iterations) Defer to follow-up work: - Admin endpoint for flag visibility - FeatureFlag component (nice-to-have) - Dynamic flag updates without restart (requires external service) - User-specific flags (A/B testing) - Flag analytics/usage tracking - Gradual rollout percentages ### Explicitly Out of Scope - Integration with Flagsmith/LaunchDarkly (future ADR) - Database-stored flags (requires schema changes) - Real-time flag updates (WebSocket/SSE) - Flag inheritance/hierarchy - Flag audit logging --- ## Testing Strategy ### Backend Tests | Test Type | Coverage Target | Location | | ----------------- | ---------------------------------------- | ------------------------------------------ | | Schema validation | Parse true/false, defaults | `src/config/env.test.ts` | | Service functions | `isFeatureEnabled`, `getAllFeatureFlags` | `src/services/featureFlags.server.test.ts` | | Integration | Admin endpoint (if added) | `src/routes/admin.routes.test.ts` | ### Frontend Tests | Test Type | Coverage Target | Location | | ------------------- | --------------------------- | ------------------------------------- | | Config structure | featureFlags section exists | `src/config.test.ts` | | Hook behavior | Returns correct values | `src/hooks/useFeatureFlag.test.ts` | | Component rendering | Conditional children | `src/components/FeatureFlag.test.tsx` | ### Mocking Pattern for Tests ```typescript // Backend - reset modules to test different flag states beforeEach(() => { vi.resetModules(); process.env.FEATURE_NEW_DASHBOARD = 'true'; }); // Frontend - mock config module vi.mock('../config', () => ({ default: { featureFlags: { newDashboard: true, betaRecipes: false, }, }, })); ``` --- ## Risk Assessment | Risk | Impact | Likelihood | Mitigation | | ------------------------------------------- | ------ | ---------- | ------------------------------------------------------------- | | Flag state inconsistency (backend/frontend) | Medium | Low | Use same env var naming, document sync requirements | | Performance impact from flag checks | Low | Low | Flags cached at startup, no runtime DB calls | | Stale flags after deployment | Medium | Medium | Document restart requirement, consider future dynamic loading | | Feature creep (too many flags) | Medium | Medium | Require ADR for new flags, sunset policy | | Missing flag causes crash | High | Low | Default to false, graceful degradation | --- ## Files to Create | File | Purpose | | ------------------------------------------ | ---------------------------- | | `src/services/featureFlags.server.ts` | Backend feature flag service | | `src/services/featureFlags.server.test.ts` | Backend tests | | `src/hooks/useFeatureFlag.ts` | React hook for flag access | | `src/hooks/useFeatureFlag.test.ts` | Hook tests | | `src/components/FeatureFlag.tsx` | Declarative flag component | | `src/components/FeatureFlag.test.tsx` | Component tests | ## Files to Modify | File | Changes | | -------------------------------------------- | ---------------------------------- | | `src/config/env.ts` | Add featureFlagsSchema and loading | | `src/config/env.test.ts` | Add feature flag tests | | `src/config.ts` | Add featureFlags section | | `src/config.test.ts` | Add feature flag tests | | `src/vite-env.d.ts` | Add VITE*FEATURE*\* declarations | | `.env.example` | Add feature flag examples | | `docs/adr/0024-feature-flagging-strategy.md` | Update status and details | | `docs/getting-started/ENVIRONMENT.md` | Document feature flag vars | | `docs/development/CODE-PATTERNS.md` | Add usage patterns | | `CLAUDE.md` | Add to quick reference | --- ## Verification Commands After implementation, run these commands in the dev container: ```bash # Type checking podman exec -it flyer-crawler-dev npm run type-check # Backend unit tests podman exec -it flyer-crawler-dev npm run test:unit -- --grep "featureFlag" # Frontend tests (includes hook and component tests) podman exec -it flyer-crawler-dev npm run test:unit -- --grep "FeatureFlag" # Full test suite podman exec -it flyer-crawler-dev npm test ``` --- ## Example Usage (Post-Implementation) ### Backend Route Handler ```typescript // src/routes/flyers.routes.ts import { isFeatureEnabled } from '../services/featureFlags.server'; router.get('/dashboard', async (req, res) => { if (isFeatureEnabled('newDashboard')) { // New dashboard logic return sendSuccess(res, { version: 'v2', data: await getNewDashboardData() }); } // Legacy dashboard return sendSuccess(res, { version: 'v1', data: await getLegacyDashboardData() }); }); ``` ### React Component ```tsx // src/pages/Dashboard.tsx import { FeatureFlag } from '../components/FeatureFlag'; import { useFeatureFlag } from '../hooks/useFeatureFlag'; // Option 1: Declarative component function Dashboard() { return ( }> ); } // Option 2: Hook for logic function DashboardWithLogic() { const isNewDashboard = useFeatureFlag('newDashboard'); useEffect(() => { if (isNewDashboard) { analytics.track('new_dashboard_viewed'); } }, [isNewDashboard]); return isNewDashboard ? : ; } ``` --- ## Implementation Notes ### Naming Convention | Context | Pattern | Example | | ---------------- | ------------------------- | ---------------------------------- | | Backend env var | `FEATURE_SNAKE_CASE` | `FEATURE_NEW_DASHBOARD` | | Frontend env var | `VITE_FEATURE_SNAKE_CASE` | `VITE_FEATURE_NEW_DASHBOARD` | | Config property | `camelCase` | `config.featureFlags.newDashboard` | | Type/Hook param | `camelCase` | `isFeatureEnabled('newDashboard')` | ### Flag Lifecycle 1. **Adding a flag**: Add to both schemas, set default to `false`, document 2. **Enabling a flag**: Set env var to `'true'`, restart application 3. **Removing a flag**: Remove conditional code first, then remove flag from schemas 4. **Sunset policy**: Flags should be removed within 3 months of full rollout --- Last updated: 2026-01-28