Files
flyer-crawler.projectium.com/docs/plans/2026-01-28-adr-024-feature-flags-implementation.md
Torben Sorensen 61f24305fb
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 22m13s
ADR-024 Feature Flagging Strategy
2026-01-28 23:23:45 -08:00

25 KiB

ADR-024 Implementation Plan: Feature Flagging Strategy

Date: 2026-01-28 Type: Technical Implementation Plan Related: ADR-024: Feature Flagging Strategy, ADR-007: Configuration and Secrets Management 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

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

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

// 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<FeatureFlagName, boolean> {
  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:

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

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

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

// 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 <NewDashboard />;
 * }
 */
export function useFeatureFlag(flagName: FeatureFlagName): boolean {
  return useMemo(() => config.featureFlags[flagName], [flagName]);
}

/**
 * Get all feature flags (useful for debugging).
 */
export function useAllFeatureFlags(): Record<FeatureFlagName, boolean> {
  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:

  • <FeatureFlag name="flagName"> component
  • Children rendered only when flag is enabled
  • Optional fallback prop for disabled state
  • TypeScript-enforced flag names

Implementation Details:

// 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
 * <FeatureFlag name="newDashboard" fallback={<OldDashboard />}>
 *   <NewDashboard />
 * </FeatureFlag>
 */
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:

// 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(
      <FeatureFlag name="newDashboard">
        <div data-testid="new-feature">New Feature</div>
      </FeatureFlag>
    );
    expect(screen.queryByTestId('new-feature')).not.toBeInTheDocument();
  });

  it('should render fallback when flag is disabled', () => {
    render(
      <FeatureFlag name="newDashboard" fallback={<div>Old Feature</div>}>
        <div>New Feature</div>
      </FeatureFlag>
    );
    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:

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

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

[2.1] Config ──> [2.2] Hook ──> [2.3] Component ──> [2.4] Frontend Tests

Phase 3 (Documentation) - After Implementation

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

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

# 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

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

// src/pages/Dashboard.tsx
import { FeatureFlag } from '../components/FeatureFlag';
import { useFeatureFlag } from '../hooks/useFeatureFlag';

// Option 1: Declarative component
function Dashboard() {
  return (
    <FeatureFlag name="newDashboard" fallback={<LegacyDashboard />}>
      <NewDashboard />
    </FeatureFlag>
  );
}

// Option 2: Hook for logic
function DashboardWithLogic() {
  const isNewDashboard = useFeatureFlag('newDashboard');

  useEffect(() => {
    if (isNewDashboard) {
      analytics.track('new_dashboard_viewed');
    }
  }, [isNewDashboard]);

  return isNewDashboard ? <NewDashboard /> : <LegacyDashboard />;
}

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