Lightweight custom feature flag system with runtime toggles, user targeting, A/B testing, and admin UI.
Core Library: app/lib/feature-flags.ts
- Flag evaluation logic with targeting rules
- LocalStorage cache + optional API sync
- Privacy-safe hashing for consistent A/B bucketing
- Tracks flag usage via monitoring breadcrumbs
Configuration: app/lib/flags.config.ts
- Default flags seeded on first load
- Production overrides via API (
NEXT_PUBLIC_API_URL/feature-flags)
Context Provider: app/context/FeatureFlagContext.tsx
- React Context for flag state
- Syncs user context (wallet address, network) for targeting
- Admin functions: toggle, update, create, delete flags
Hooks: app/hooks/useFeatureFlag.ts
useFeatureFlag(key)— boolean flagsuseFeatureFlagValue<T>(key)— multivariate flagsuseFeatureFlagMany(keys)— batch check
Admin UI: app/components/FeatureFlagAdmin.tsx
- Slide-in panel accessible via TopNav flag icon (dev only)
- Search, filter (all/enabled/disabled/kill_switch/experiment/beta)
- Live toggle, rollout percentage slider, kill switch controls
- Create/delete flags at runtime
- boolean — simple on/off
- rollout — gradual percentage-based rollout (0-100%)
- string — multivariate (e.g., A/B button color)
- number — numeric config values
Flags can target:
- Specific users — by wallet address (truncated for privacy)
- Networks — public vs testnet
- Segments — user groups (beta_tester, internal, etc.)
- Percentage rollout — consistent bucketing via hash of
flagKey:userId
Kill switches override all targeting and force-disable.
import { useFeatureFlag } from "@/hooks/useFeatureFlag";
export default function Dashboard() {
const { isEnabled, isLoading } = useFeatureFlag("new-dashboard-layout");
if (isLoading) return <Skeleton />;
if (isEnabled) return <NewDashboard />;
return <OldDashboard />;
}Option 1: Via Admin UI
- Open dashboard
- Click flag icon in TopNav (dev mode)
- Click "New Flag"
- Fill in key, name, description, type
- Click "Create"
Option 2: In Code
Add to app/lib/flags.config.ts:
{
key: "my-new-feature",
name: "My New Feature",
description: "Description of what this controls.",
defaultValue: false,
type: "boolean",
enabled: false,
tags: { team: "frontend", area: "dashboard" },
}If NEXT_PUBLIC_API_URL is set, flags sync with backend:
GET /feature-flags— load all flagsPUT /feature-flags/:key— update flagPOST /feature-flags— create flagDELETE /feature-flags/:key— delete flag
Without API: Flags persist in localStorage only (client-side).
Every flag evaluation tracks a breadcrumb:
{
"message": "feature_flag_evaluated",
"category": "user_action",
"data": {
"flag": "new-dashboard-layout",
"value": "true",
"reason": "rollout_included_42",
"targeted": true
}
}View in Sentry's breadcrumb trail for debugging.
See app/lib/flags.config.ts for all configured flags:
- new-dashboard-layout — redesigned dashboard (rollout)
- enhanced-charts — new chart library
- wallet-staking — staking UI
- ab-new-onboarding — A/B test 50% rollout
- ab-cta-button-color — string flag (teal vs green)
- disable-referral-system — kill switch
- disable-governance — kill switch
- beta-analytics-v2 — beta testers only
- testnet-only-features — testnet network targeting
- websocket-balances — performance toggle
Nestera/frontend/app/
├── lib/
│ ├── feature-flags.ts # Core engine
│ └── flags.config.ts # Default flags
├── context/
│ └── FeatureFlagContext.tsx # Provider
├── hooks/
│ └── useFeatureFlag.ts # React hooks
├── components/
│ └── FeatureFlagAdmin.tsx # Admin UI
└── dashboard/
├── DashboardProviders.tsx # Wraps layout
└── layout.tsx # Updated to use provider
FeatureFlagProvider is wired into /dashboard layout to bridge server layout → client context, with wallet user context passed automatically.
- Privacy-safe: Wallet addresses truncated to first 10 chars
- No sensitive data: Keys/values never contain secrets
- Admin UI: Dev mode only (
process.env.NODE_ENV !== 'production') - API auth: Backend endpoints should require authentication
- Flags cached in LocalStorage (instant load)
- API fetch in background (non-blocking)
- Evaluation is synchronous (no async waterfall)
- No re-renders unless flag values change
Flag not showing in UI?
- Check
flags.config.tsis imported - Verify
initFeatureFlags()called in provider - Check browser console for errors
Admin UI not visible?
- Only visible in dev mode (
NODE_ENV !== 'production') - Check TopNav for flag icon
User targeting not working?
- Ensure
WalletContextprovides address/network - Check
DashboardProviderspassesuserContextprop
- Server-side flag evaluation (SSR)
- Redis cache for production flags
- Flag scheduling (enable at specific date/time)
- Segment management UI
- Flag dependency graph (flag X requires flag Y)
- Metrics dashboard (flag adoption rates)