> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Feature Flags — Integration Guide

> Version: 1.0.0 Status: ✅ Complete Module: PF (Platform Foundation) Spec: specs/pf/specs/PF-45-feature-flags-experimentation.md

**Version:** 1.0.0\
**Status:** ✅ Complete\
**Module:** PF (Platform Foundation)\
**Spec:** `specs/pf/specs/PF-45-feature-flags-experimentation.md`

***

## Overview

PF-45 provides a centralized feature flag system for progressive rollouts, kill-switches, and A/B testing across all cores. Flags are evaluated via:

* **React hook** — `useFeatureFlag` (client-side, TanStack Query–backed)
* **RPC** — `pf_check_feature_flag` (server-side / edge functions)

All flags are tenant-scoped or global. The system **fails safe** — if evaluation errors, `enabled` defaults to `false`.

***

## Consumer Guide: React Hook

### Import

```typescript theme={null}
import { useFeatureFlag } from '@/platform/feature-flags';
```

### Signature

```typescript theme={null}
function useFeatureFlag(flagKey: string): {
  enabled: boolean;       // Whether the flag is on for the current user/org
  variant: string | null; // A/B test variant key, or null for boolean/percentage flags
  isLoading: boolean;
  error: Error | null;
};
```

### Caching

| Setting     | Value                                                              |
| ----------- | ------------------------------------------------------------------ |
| `staleTime` | Derived from `feature_flag_cache_ttl_minutes` (default: 5 minutes) |
| `gcTime`    | 2× `staleTime` (default: 10 minutes)                               |
| `retry`     | 1                                                                  |
| Query key   | `['feature-flag', flagKey, userId, orgId]`                         |
| Fail-safe   | `{ enabled: false, variant: null }`                                |

### Examples

#### Boolean flag (kill-switch / feature gate)

```tsx theme={null}
import { useFeatureFlag } from '@/platform/feature-flags';

function MyComponent() {
  const { enabled } = useFeatureFlag('cl.new_chart_layout');

  if (!enabled) return <LegacyChart />;
  return <NewChart />;
}
```

#### Percentage rollout

```tsx theme={null}
const { enabled } = useFeatureFlag('pf.progressive_sidebar');
// `enabled` is true for the configured percentage of users
// Evaluation is deterministic per user — same user always gets the same result
```

#### A/B test with variants

```tsx theme={null}
const { enabled, variant } = useFeatureFlag('ce.onboarding_experiment');

if (!enabled) return <DefaultOnboarding />;

switch (variant) {
  case 'streamlined':
    return <StreamlinedOnboarding />;
  case 'guided':
    return <GuidedOnboarding />;
  default:
    return <DefaultOnboarding />;
}
```

Variants are configured as `{ key: string; weight: number }[]` on the flag. The RPC assigns a variant based on weighted random distribution, deterministic per user.

***

## Consumer Guide: RPC (Edge Functions / Server-Side)

### Function

```sql theme={null}
pf_check_feature_flag(
  p_flag_key      TEXT,
  p_user_id       UUID DEFAULT NULL,
  p_organization_id UUID DEFAULT NULL
) RETURNS JSONB
```

### Return shape

```json theme={null}
{ "enabled": true, "variant": "streamlined" }
```

or

```json theme={null}
{ "enabled": false, "variant": null }
```

### Usage from an edge function

```typescript theme={null}
import { createClient } from 'npm:@supabase/supabase-js@2';

// Security: Verify request JWT, extract userId/orgId server-side
// Enforce admin/RBAC checks or use row-level security
// Only use service role in trusted backend contexts
const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);

const { data, error } = await supabase.rpc('pf_check_feature_flag', {
  p_flag_key: 'fa.new_invoice_format',
  p_user_id: userId,
  p_organization_id: orgId,
});

if (error || !data?.enabled) {
  // Fall back to existing behavior
}
```

***

## Flag Key Convention

All flag keys follow the pattern:

```
{core}.{feature_name}
```

| Core | Example Key                | Purpose                          |
| ---- | -------------------------- | -------------------------------- |
| `pf` | `pf.dark_mode`             | Platform-wide UI toggle          |
| `cl` | `cl.new_chart_layout`      | Clinical module feature          |
| `ce` | `ce.onboarding_experiment` | A/B test in Community Engagement |
| `fa` | `fa.new_invoice_format`    | Finance feature gate             |

Keys must be lowercase, underscore-separated, and prefixed with the owning core abbreviation.

***

## Consuming Cores

| Consumer | Flag(s)                | Integration Type | Status     |
| -------- | ---------------------- | ---------------- | ---------- |
| CL-35    | Clinical feature gates | React hook       | 📝 Planned |
| PF-31    | Platform feature gates | React hook       | 📝 Planned |
| PF-52    | Platform feature gates | React hook       | 📝 Planned |

***

## Anti-Patterns

### ❌ Direct table query from another core

```typescript theme={null}
// ❌ WRONG: Querying pf_feature_flags directly
const { data } = await supabase
  .from('pf_feature_flags')
  .select('is_enabled')
  .eq('flag_key', 'cl.my_flag');

// ✅ CORRECT: Use the hook or RPC
const { enabled } = useFeatureFlag('cl.my_flag');
```

### ❌ Separate caching layer

```typescript theme={null}
// ❌ WRONG: Redundant cache — the hook already caches via TanStack Query
const cached = localStorage.getItem('flag_cl.my_flag');

// ✅ CORRECT: Just use the hook; it handles stale/gc times
const { enabled } = useFeatureFlag('cl.my_flag');
```

### ❌ Hardcoded flag checks

```typescript theme={null}
// ❌ WRONG: Hardcoded boolean instead of flag
const ENABLE_NEW_FEATURE = true;

// ✅ CORRECT: Use a flag so it can be toggled without a deploy
const { enabled } = useFeatureFlag('hr.new_feature');
```

### ❌ Flag evaluation in render-critical paths without loading state

```tsx theme={null}
// ❌ WRONG: No loading guard — will flash default then switch
return enabled ? <NewUI /> : <OldUI />;

// ✅ CORRECT: Show skeleton while flag is resolving
if (isLoading) return <Skeleton className="h-48 w-full" />;
return enabled ? <NewUI /> : <OldUI />;
```

***

## Related Documentation

* Spec: `specs/pf/specs/PF-45-feature-flags-experimentation.md`
* Plan: `specs/pf/plans/PF-45-feature-flags-experimentation-PLAN.md`
* Tasks: `specs/pf/tasks/PF-45-TASKS.md`
* Platform AGENTS: `src/platform/AGENTS.md` § Feature Flags
* Cross-core integrations: `docs/architecture/integrations/CROSS_CORE_INTEGRATIONS.md`
