Skip to main content

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.

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 hookuseFeatureFlag (client-side, TanStack Query–backed)
  • RPCpf_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

import { useFeatureFlag } from '@/platform/feature-flags';

Signature

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

SettingValue
staleTimeDerived from feature_flag_cache_ttl_minutes (default: 5 minutes)
gcTimestaleTime (default: 10 minutes)
retry1
Query key['feature-flag', flagKey, userId, orgId]
Fail-safe{ enabled: false, variant: null }

Examples

Boolean flag (kill-switch / feature gate)

import { useFeatureFlag } from '@/platform/feature-flags';

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

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

Percentage rollout

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

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

pf_check_feature_flag(
  p_flag_key      TEXT,
  p_user_id       UUID DEFAULT NULL,
  p_organization_id UUID DEFAULT NULL
) RETURNS JSONB

Return shape

{ "enabled": true, "variant": "streamlined" }
or
{ "enabled": false, "variant": null }

Usage from an edge function

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}
CoreExample KeyPurpose
pfpf.dark_modePlatform-wide UI toggle
clcl.new_chart_layoutClinical module feature
cece.onboarding_experimentA/B test in Community Engagement
fafa.new_invoice_formatFinance feature gate
Keys must be lowercase, underscore-separated, and prefixed with the owning core abbreviation.

Consuming Cores

ConsumerFlag(s)Integration TypeStatus
CL-35Clinical feature gatesReact hook📝 Planned
PF-31Platform feature gatesReact hook📝 Planned
PF-52Platform feature gatesReact hook📝 Planned

Anti-Patterns

❌ Direct table query from another core

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

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

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

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

  • 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