# Feature Flag System

> ArchiTools internal architecture reference -- feature flag design, implementation, and usage patterns.

---

## Overview

The feature flag system controls which modules and capabilities are available at runtime. It serves three purposes:

1. **Module gating** -- a module whose flag is disabled is never imported, never bundled into the active page, and never visible in navigation.
2. **Incremental rollout** -- experimental features can be enabled for specific users or environments without a separate deployment.
3. **Operational control** -- system flags allow disabling capabilities (e.g., AI features) without a code change.

Flags are defined statically in `src/config/flags.ts`, consumed via a React context provider, and checked with a hook or a gate component. There is no remote flag service today; the system is designed so one can be added without changing consumer code.

---

## Flag Definition Model

```typescript
// src/types/flags.ts

type FlagCategory = 'module' | 'experimental' | 'system';

interface FeatureFlag {
  key: string;                   // unique identifier, matches module's featureFlag field
  enabled: boolean;              // default state
  label: string;                 // Romanian display name
  description: string;           // Romanian description of what this flag controls
  category: FlagCategory;        // classification for the admin UI
  requiredRole?: Role;           // minimum role to see this feature (future use)
  dependencies?: string[];       // other flag keys that must be enabled for this flag to take effect
  overridable: boolean;          // whether this flag can be toggled at runtime via admin panel
}
```

### Field Details

| Field | Notes |
|---|---|
| `key` | Convention: `module.[module-id]` for module flags, `exp.[feature]` for experimental, `sys.[capability]` for system. |
| `enabled` | The compiled default. This is what ships. Can be overridden per-environment via env vars or at runtime. |
| `dependencies` | If flag A depends on flag B, enabling A while B is disabled has no effect -- the system treats A as disabled. |
| `overridable` | System-critical flags (e.g., `sys.storage-engine`) should be `false` to prevent accidental runtime changes. |
| `requiredRole` | Not enforced today. Reserved for role-based flag filtering when the auth system is implemented. |

---

## Default Flags Configuration

All flags are declared in a single file. This is the source of truth.

```typescript
// src/config/flags.ts

import type { FeatureFlag } from '@/types/flags';

export const defaultFlags: FeatureFlag[] = [
  // ─── Module Flags ──────────────────────────────────────────
  {
    key: 'module.devize-generator',
    enabled: true,
    label: 'Generator Devize',
    description: 'Activeaza modulul de generare devize.',
    category: 'module',
    overridable: true,
  },
  {
    key: 'module.oferte',
    enabled: true,
    label: 'Oferte',
    description: 'Activeaza modulul de creare si gestionare oferte.',
    category: 'module',
    overridable: true,
  },
  {
    key: 'module.pontaj',
    enabled: true,
    label: 'Pontaj',
    description: 'Activeaza modulul de pontaj si evidenta ore.',
    category: 'module',
    overridable: true,
  },

  // ─── Experimental Flags ────────────────────────────────────
  {
    key: 'exp.ai-assistant',
    enabled: false,
    label: 'Asistent AI',
    description: 'Activeaza asistentul AI experimental pentru generare de continut.',
    category: 'experimental',
    overridable: true,
  },
  {
    key: 'exp.dark-mode',
    enabled: false,
    label: 'Mod Intunecat',
    description: 'Activeaza tema intunecata (in testare).',
    category: 'experimental',
    overridable: true,
  },

  // ─── System Flags ─────────────────────────────────────────
  {
    key: 'sys.offline-mode',
    enabled: true,
    label: 'Mod Offline',
    description: 'Permite functionarea aplicatiei fara conexiune la internet.',
    category: 'system',
    overridable: false,
  },
  {
    key: 'sys.analytics',
    enabled: false,
    label: 'Analitice',
    description: 'Colectare de date de utilizare anonimizate.',
    category: 'system',
    overridable: false,
  },
];

/** Build a lookup map for O(1) access by key */
export const flagDefaults: Map<string, FeatureFlag> = new Map(
  defaultFlags.map((flag) => [flag.key, flag])
);
```

---

## FeatureFlagProvider

The provider initializes flag state from defaults, applies environment variable overrides, applies runtime overrides from localStorage, resolves dependencies, and exposes the resolved state via context.

```typescript
// src/providers/FeatureFlagProvider.tsx

'use client';

import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';
import { defaultFlags, flagDefaults } from '@/config/flags';
import type { FeatureFlag } from '@/types/flags';

interface FlagState {
  /** Check if a flag is enabled (dependency-resolved) */
  isEnabled: (key: string) => boolean;
  /** Get the full flag definition */
  getFlag: (key: string) => FeatureFlag | undefined;
  /** Set a runtime override (only for overridable flags) */
  setOverride: (key: string, enabled: boolean) => void;
  /** Clear a runtime override */
  clearOverride: (key: string) => void;
  /** All flags with their resolved states */
  allFlags: FeatureFlag[];
}

const FeatureFlagContext = createContext<FlagState | null>(null);

const STORAGE_KEY = 'architools.flag-overrides';

function loadOverrides(): Record<string, boolean> {
  if (typeof window === 'undefined') return {};
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : {};
  } catch {
    return {};
  }
}

function saveOverrides(overrides: Record<string, boolean>): void {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(overrides));
}

function getEnvOverride(key: string): boolean | undefined {
  // Convention: NEXT_PUBLIC_FLAG_MODULE_DEVIZE_GENERATOR=true
  // Transform flag key: "module.devize-generator" -> "MODULE_DEVIZE_GENERATOR"
  const envKey = `NEXT_PUBLIC_FLAG_${key.replace(/[.-]/g, '_').toUpperCase()}`;

  const value = process.env[envKey];
  if (value === 'true') return true;
  if (value === 'false') return false;
  return undefined;
}

export function FeatureFlagProvider({ children }: { children: React.ReactNode }) {
  const [overrides, setOverrides] = useState<Record<string, boolean>>(loadOverrides);

  const resolvedFlags = useMemo(() => {
    // Step 1: Compute effective enabled state for each flag
    const effective = new Map<string, boolean>();

    for (const flag of defaultFlags) {
      // Priority: env override > runtime override > compiled default
      const envOverride = getEnvOverride(flag.key);
      const runtimeOverride = overrides[flag.key];
      const baseValue = envOverride ?? runtimeOverride ?? flag.enabled;
      effective.set(flag.key, baseValue);
    }

    // Step 2: Resolve dependencies (a flag is only enabled if all its dependencies are enabled)
    const resolved = new Map<string, boolean>();

    function resolve(key: string, visited: Set<string> = new Set()): boolean {
      if (resolved.has(key)) return resolved.get(key)!;
      if (visited.has(key)) return false; // circular dependency -- fail closed
      visited.add(key);

      const flag = flagDefaults.get(key);
      if (!flag) return false;

      let enabled = effective.get(key) ?? false;

      if (enabled && flag.dependencies?.length) {
        enabled = flag.dependencies.every((dep) => resolve(dep, visited));
      }

      resolved.set(key, enabled);
      return enabled;
    }

    for (const flag of defaultFlags) {
      resolve(flag.key);
    }

    return resolved;
  }, [overrides]);

  const setOverride = useCallback((key: string, enabled: boolean) => {
    const flag = flagDefaults.get(key);
    if (!flag?.overridable) {
      console.warn(`[FeatureFlags] Flag "${key}" is not overridable.`);
      return;
    }
    setOverrides((prev) => {
      const next = { ...prev, [key]: enabled };
      saveOverrides(next);
      return next;
    });
  }, []);

  const clearOverride = useCallback((key: string) => {
    setOverrides((prev) => {
      const { [key]: _, ...rest } = prev;
      saveOverrides(rest);
      return rest;
    });
  }, []);

  const contextValue = useMemo<FlagState>(() => ({
    isEnabled: (key: string) => resolvedFlags.get(key) ?? false,
    getFlag: (key: string) => flagDefaults.get(key),
    setOverride,
    clearOverride,
    allFlags: defaultFlags.map((f) => ({
      ...f,
      enabled: resolvedFlags.get(f.key) ?? false,
    })),
  }), [resolvedFlags, setOverride, clearOverride]);

  return (
    <FeatureFlagContext.Provider value={contextValue}>
      {children}
    </FeatureFlagContext.Provider>
  );
}

export function useFeatureFlagContext(): FlagState {
  const context = useContext(FeatureFlagContext);
  if (!context) {
    throw new Error('useFeatureFlagContext must be used within a FeatureFlagProvider');
  }
  return context;
}
```

### Provider Placement

The provider wraps the entire application layout:

```typescript
// src/app/layout.tsx

import { FeatureFlagProvider } from '@/providers/FeatureFlagProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ro">
      <body>
        <FeatureFlagProvider>
          {children}
        </FeatureFlagProvider>
      </body>
    </html>
  );
}
```

---

## `useFeatureFlag` Hook

The primary consumer API. Returns a boolean.

```typescript
// src/hooks/useFeatureFlag.ts

'use client';

import { useFeatureFlagContext } from '@/providers/FeatureFlagProvider';

/**
 * Check if a feature flag is enabled.
 * Returns false for unknown keys (fail closed).
 */
export function useFeatureFlag(key: string): boolean {
  const { isEnabled } = useFeatureFlagContext();
  return isEnabled(key);
}
```

### Usage

```typescript
function SomeComponent() {
  const aiEnabled = useFeatureFlag('exp.ai-assistant');

  return (
    <div>
      {aiEnabled && <AiSuggestionPanel />}
    </div>
  );
}
```

---

## `FeatureGate` Component

A declarative alternative to the hook, useful when you want to conditionally render a subtree without introducing a new component.

```typescript
// src/components/FeatureGate.tsx

'use client';

import { useFeatureFlag } from '@/hooks/useFeatureFlag';

interface FeatureGateProps {
  flag: string;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

/**
 * Renders children only if the specified flag is enabled.
 * Optionally renders a fallback when disabled.
 */
export function FeatureGate({ flag, children, fallback = null }: FeatureGateProps) {
  const enabled = useFeatureFlag(flag);
  return <>{enabled ? children : fallback}</>;
}
```

### Usage

```tsx
<FeatureGate flag="exp.dark-mode">
  <ThemeToggle />
</FeatureGate>

<FeatureGate flag="module.pontaj" fallback={<ModuleDisabledNotice />}>
  <PontajWidget />
</FeatureGate>
```

---

## Flags and Module Loading

The critical integration point: **a disabled flag means the module's JavaScript is never fetched**.

This works because of the layered architecture:

1. **Navigation**: The sidebar queries `useFeatureFlag(module.featureFlag)` for each registered module. Disabled modules are excluded from the nav -- the user has no link to click.

2. **Route guard**: The module's route page checks the flag before rendering the `ModuleLoader`. If the flag is disabled, it renders a 404 or redirect.

```typescript
// src/app/(modules)/devize/page.tsx

import { ModuleLoader } from '@/lib/module-loader';
import { moduleRegistry } from '@/config/modules';
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
import { notFound } from 'next/navigation';

export default function DevizePage() {
  const config = moduleRegistry.get('devize-generator')!;
  const enabled = useFeatureFlag(config.featureFlag);

  if (!enabled) {
    notFound();
  }

  return <ModuleLoader config={config} />;
}
```

3. **Lazy import**: `React.lazy(() => import(...))` only executes the import when the component renders. Since the route guard prevents rendering when the flag is off, the dynamic `import()` never fires, and the browser never requests the chunk.

**Result**: Disabling a module flag removes it from the bundle that the user downloads. There is zero performance cost for disabled modules.

---

## Runtime Override Mechanism

Overridable flags can be toggled at runtime through the admin panel or the browser console. Overrides are stored in `localStorage` under the key `architools.flag-overrides`.

### Admin Panel

The admin panel (planned) will render all overridable flags as toggles, grouped by category. It uses `setOverride` and `clearOverride` from the context.

### Console API (Development)

For development and debugging, flags can be manipulated directly:

```javascript
// In the browser console:

// Enable a flag
localStorage.setItem('architools.flag-overrides',
  JSON.stringify({ ...JSON.parse(localStorage.getItem('architools.flag-overrides') || '{}'), 'exp.ai-assistant': true })
);
// Then reload the page.
```

A convenience utility can be exposed on `window` in development builds:

```typescript
// src/lib/dev-tools.ts

if (process.env.NODE_ENV === 'development') {
  (window as any).__flags = {
    enable: (key: string) => { /* calls setOverride */ },
    disable: (key: string) => { /* calls setOverride */ },
    reset: (key: string) => { /* calls clearOverride */ },
    list: () => { /* logs all flags with their resolved states */ },
  };
}
```

---

## Flag Categories

### Module Flags (`module.*`)

One flag per module. Controls whether the module is loaded and visible. Key must match the `featureFlag` field in the module's `ModuleConfig`.

Examples: `module.devize-generator`, `module.oferte`, `module.pontaj`.

### Experimental Flags (`exp.*`)

Control features that are under active development or testing. These may cut across modules or affect platform-level behavior. Expected to eventually become permanent (either promoted to always-on or removed).

Examples: `exp.ai-assistant`, `exp.dark-mode`.

### System Flags (`sys.*`)

Control infrastructure-level capabilities. Typically not overridable. Changing these may require understanding of downstream effects.

Examples: `sys.offline-mode`, `sys.analytics`.

---

## Environment Variable Overrides

For deployment-time control, flags can be overridden via environment variables. This is useful for:

- Enabling experimental features in staging but not production.
- Disabling a broken module without a code change.
- Per-environment flag profiles in Docker Compose.

### Convention

```
NEXT_PUBLIC_FLAG_<NORMALIZED_KEY>=true|false
```

The key is normalized by replacing `.` and `-` with `_` and uppercasing:

| Flag Key | Environment Variable |
|---|---|
| `module.devize-generator` | `NEXT_PUBLIC_FLAG_MODULE_DEVIZE_GENERATOR` |
| `exp.ai-assistant` | `NEXT_PUBLIC_FLAG_EXP_AI_ASSISTANT` |
| `sys.offline-mode` | `NEXT_PUBLIC_FLAG_SYS_OFFLINE_MODE` |

### Priority Order

```
Environment variable  >  Runtime override (localStorage)  >  Compiled default
```

Environment variables take the highest priority because they represent a deployment decision that should not be overridden by stale localStorage state.

### Docker Compose Example

```yaml
# docker-compose.staging.yml
services:
  web:
    environment:
      - NEXT_PUBLIC_FLAG_EXP_AI_ASSISTANT=true
      - NEXT_PUBLIC_FLAG_SYS_ANALYTICS=true
```

---

## Anti-Patterns

### 1. Direct Module Import (bypassing flags)

```typescript
// WRONG: This imports the module unconditionally -- it will be in the bundle
// regardless of the flag state.
import { DevizeGenerator } from '@/modules/devize-generator';
```

Always go through the flag-gated `ModuleLoader` or use `FeatureGate`/`useFeatureFlag`.

### 2. Checking Flags Outside React

```typescript
// WRONG: This reads the default, not the resolved state with overrides.
import { flagDefaults } from '@/config/flags';
if (flagDefaults.get('module.pontaj')?.enabled) { ... }
```

Always use `useFeatureFlag()` inside a component or `useFeatureFlagContext()` to get the resolved state.

### 3. Hardcoding Flag Values

```typescript
// WRONG: This defeats the purpose of having flags.
const isAiEnabled = process.env.NODE_ENV === 'production' ? false : true;
```

Use the flag system. If you need environment-specific behavior, use the env var override mechanism.

### 4. Flag Key Mismatch

The flag `key` in `flags.ts` must exactly match the `featureFlag` in the module's `config.ts`. A mismatch means the module can never be gated. The registry validation (`validateRegistry`) does not currently check this -- it is enforced by code review and the module development checklist.

### 5. Non-Overridable Flags in the Admin Panel

Do not expose `overridable: false` flags in the admin toggle UI. The provider already guards against this, but the UI should also filter them out.

---

## Future: Remote Flag Service

The current system is local-first by design. When a remote flag service is needed (e.g., for per-user rollout, A/B testing, or centralized control), the integration point is the `FeatureFlagProvider`:

1. On mount, the provider fetches flags from the remote service.
2. Remote values are merged into the resolution chain with a priority between env vars and runtime overrides:
   ```
   Env var  >  Remote service  >  Runtime override  >  Compiled default
   ```
3. The provider caches remote flags in localStorage for offline resilience.
4. Consumer code (`useFeatureFlag`, `FeatureGate`) requires zero changes.

The `FeatureFlag` interface and provider API are designed to accommodate this without breaking changes.
