Skip to content

Feature Flag System ​

DoCurious includes a built-in feature flag system with three layers: a Zustand store for flag management, a React hook for evaluation, and a declarative gate component for conditional rendering.

Architecture ​

Store: useFeatureFlagStore ​

Defined in src/store/useFeatureFlagStore.ts. Manages feature flag definitions, CRUD operations, and flag evaluation. Persists to localStorage via Zustand's persist middleware (key: docurious-feature-flags).

State ​

typescript
interface FeatureFlagState {
  flags: FeatureFlag[]
  isLoading: boolean
  error: string | null
}

Actions ​

typescript
interface FeatureFlagActions {
  fetchFlags: () => void
  createFlag: (data: CreateFeatureFlagData) => void
  updateFlag: (id: string, data: UpdateFeatureFlagData) => void
  deleteFlag: (id: string) => void
  toggleFlag: (id: string) => void
  evaluateFlag: (flagKey: string, context: FlagEvaluationContext) => FlagEvaluationResult
  clearError: () => void
}

Selectors ​

typescript
selectFlags         // All flags
selectActiveFlags   // Only active flags
selectFlagByKey(key) // Single flag by key (returns selector function)

Flag Types ​

Flags support three types:

TypeDescriptionValue
booleanSimple on/offtrue or false
multivariateA/B test with weighted variantsVariant value (string)
jsonArbitrary JSON payloadJSON object

Targeting Rules ​

Each flag can have multiple targeting rules (AND logic -- all must pass):

Rule TypeOperatorsExample
rolein, not_inShow only to platform_admin
user_idin, not_inTarget specific users
account_tierin, not_inTarget tier_2_full only
percentagepercentage_lteRoll out to 25% of users
environmentin, not_inOnly in development

Percentage rollouts use deterministic hashing (userId:flagKey) to ensure consistent assignment.

Seed Flags ​

The store ships with four seed flags:

KeyNameTypeStatusTargeting
wallet_v2Wallet V2booleanactiveRole: platform_admin only
beta_challengesBeta Challengesbooleanactive25% rollout
dark_modeDark ModebooleaninactiveNone (disabled)
new_onboardingNew Onboarding FlowmultivariateactiveExclude admin/staff; 50/50 control vs streamlined

Hook: useFeatureFlag ​

Defined in src/hooks/useFeatureFlag.ts. Bridges the auth store (user context) with the feature flag store (evaluation).

Usage ​

typescript
import { useFeatureFlag } from '../hooks/useFeatureFlag'

function MyComponent() {
  const { enabled, value, isLoading } = useFeatureFlag('wallet_v2')

  if (isLoading) return <Spinner />
  if (!enabled) return <OldWallet />
  return <NewWallet />
}

Return Type ​

typescript
interface UseFeatureFlagReturn {
  enabled: boolean   // Whether the flag is enabled for the current user
  value: unknown     // Resolved value (boolean, variant string, or JSON)
  isLoading: boolean // Whether the flag store is still loading
}

How It Works ​

The hook:

  1. Reads the current user from useAuthStore
  2. Constructs a FlagEvaluationContext with userId, role, accountTier, and environment
  3. Calls evaluateFlag on the feature flag store
  4. Memoizes the result to avoid unnecessary recalculations
typescript
const context: FlagEvaluationContext = {
  userId: user.id,
  role: user.role,
  accountTier: user.accountTier,
  environment: import.meta.env.MODE ?? 'development',
}

If no user is logged in, the hook returns { enabled: false, value: false }.

Component: FeatureGate ​

Defined in src/components/common/FeatureGate.tsx. Declarative component for conditional rendering based on feature flags.

Usage ​

tsx
import { FeatureGate } from '../components/common'

// Basic usage — render children only if flag is enabled
<FeatureGate flag="wallet_v2">
  <NewWalletDashboard />
</FeatureGate>

// With fallback — render alternative when flag is disabled
<FeatureGate flag="wallet_v2" fallback={<OldWalletDashboard />}>
  <NewWalletDashboard />
</FeatureGate>

Props ​

typescript
interface FeatureGateProps {
  flag: string              // The flag key to evaluate
  children: React.ReactNode // Content when flag is enabled
  fallback?: React.ReactNode // Content when flag is disabled (default: null)
}

Admin Management ​

Feature flags are managed at /admin/feature-flags (requires platform_admin role). The admin UI allows:

  • Viewing all flags with status, type, and targeting rules
  • Creating new flags with key, name, description, type, and targeting rules
  • Editing existing flags (key uniqueness is enforced)
  • Toggling flags between active/inactive
  • Deleting flags

Defining a New Flag ​

Via the Admin UI ​

  1. Navigate to /admin/feature-flags
  2. Click "Create Flag"
  3. Fill in: key (unique, snake_case), name, description, type
  4. Add targeting rules if needed
  5. Set status to active or inactive

Programmatically ​

typescript
import { useFeatureFlagStore } from '../store'

const createFlag = useFeatureFlagStore.getState().createFlag

createFlag({
  key: 'my_new_feature',
  name: 'My New Feature',
  description: 'Description of what this flag controls',
  type: 'boolean',
  status: 'inactive',
  defaultValue: false,
  targetingRules: [
    { id: 'rule-1', type: 'role', operator: 'in', values: ['platform_admin'] },
  ],
  tags: ['beta'],
})

Evaluation Logic ​

When evaluateFlag is called:

  1. If the flag is not found or status !== 'active', return { enabled: false, value: defaultValue }
  2. If no targeting rules exist, return { enabled: true, value: defaultValue }
  3. All targeting rules must pass (AND logic)
  4. For multivariate flags, a deterministic hash selects a variant based on cumulative weights
  5. For JSON flags, the jsonValue payload is returned
  6. For boolean flags, the defaultValue is returned

Backend Integration ​

Currently, flags are persisted to localStorage. When a real backend is available:

  1. Replace the persist middleware with API calls in fetchFlags
  2. Replace createFlag/updateFlag/deleteFlag with API mutations
  3. The evaluateFlag logic can remain client-side or move to a server-side evaluation endpoint

See Also ​

DoCurious Platform Documentation