Appearance
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:
| Type | Description | Value |
|---|---|---|
boolean | Simple on/off | true or false |
multivariate | A/B test with weighted variants | Variant value (string) |
json | Arbitrary JSON payload | JSON object |
Targeting Rules
Each flag can have multiple targeting rules (AND logic -- all must pass):
| Rule Type | Operators | Example |
|---|---|---|
role | in, not_in | Show only to platform_admin |
user_id | in, not_in | Target specific users |
account_tier | in, not_in | Target tier_2_full only |
percentage | percentage_lte | Roll out to 25% of users |
environment | in, not_in | Only in development |
Percentage rollouts use deterministic hashing (userId:flagKey) to ensure consistent assignment.
Seed Flags
The store ships with four seed flags:
| Key | Name | Type | Status | Targeting |
|---|---|---|---|---|
wallet_v2 | Wallet V2 | boolean | active | Role: platform_admin only |
beta_challenges | Beta Challenges | boolean | active | 25% rollout |
dark_mode | Dark Mode | boolean | inactive | None (disabled) |
new_onboarding | New Onboarding Flow | multivariate | active | Exclude 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:
- Reads the current user from
useAuthStore - Constructs a
FlagEvaluationContextwithuserId,role,accountTier, andenvironment - Calls
evaluateFlagon the feature flag store - 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
- Navigate to
/admin/feature-flags - Click "Create Flag"
- Fill in: key (unique, snake_case), name, description, type
- Add targeting rules if needed
- Set status to
activeorinactive
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:
- If the flag is not found or
status !== 'active', return{ enabled: false, value: defaultValue } - If no targeting rules exist, return
{ enabled: true, value: defaultValue } - All targeting rules must pass (AND logic)
- For multivariate flags, a deterministic hash selects a variant based on cumulative weights
- For JSON flags, the
jsonValuepayload is returned - For boolean flags, the
defaultValueis returned
Backend Integration
Currently, flags are persisted to localStorage. When a real backend is available:
- Replace the
persistmiddleware with API calls infetchFlags - Replace
createFlag/updateFlag/deleteFlagwith API mutations - The
evaluateFlaglogic can remain client-side or move to a server-side evaluation endpoint
See Also
- Admin guide -- feature flag management in the platform admin context
- Platform Admin role guide -- admin capabilities and permissions