On this page · sections
Feature Flags — Client-Side Patterns
Feature flags decouple deploy from release — code ships dark, a flag turns it on per user, segment, or percentage. Client evaluation must be fast, cacheable, and consistent with server authorization.
| Flag type | Purpose | Client role |
|---|---|---|
| Release | Hide incomplete features | UX gating |
| Experiment | A/B variants | Variant UI + analytics |
| Ops | Kill-switch, maintenance | Route/banner gating |
| Entitlement | Plan tier, beta cohort | Must match server auth |
Why Feature Flags
Definition
'use client';
import { useFeatureFlag } from '@/lib/flags';
export function CheckoutPage() {
const expressPayEnabled = useFeatureFlag('express-pay');
return (
<main>
<StandardCheckout />
{expressPayEnabled ? <ExpressPayButton /> : null}
</main>
);
}const FLAGS = {
'express-pay': { default: false, type: 'release' },
'checkout-copy-v2': { default: false, type: 'experiment' },
'maintenance-banner': { default: false, type: 'ops' },
};Deploy code — flag off → evaluate per user → UI hidden or visible → monitor on releaseClient role is UX gating — never the sole security boundary. Server must enforce authorization regardless of client flag state.
Separate deploy (code in prod) from release (flag on) — the senior framing.
Every flag needs owner, default, and sunset date. Flag debt confuses debugging for years.
Interview: Client flags gate UX; server enforces security. Mismatched flag state causes authorization bugs.
Client Evaluation & Bootstrap
Definition
On app init, fetch or inline a flag snapshot for the current user context — user id, anonymous id, attributes.
'use client';
import { createContext, useContext, useMemo, type ReactNode } from 'react';
type FlagSnapshot = Record<string, boolean | string>;
const FlagsContext = createContext<FlagSnapshot>({});
export function FlagsProvider({
snapshot,
children,
}: {
snapshot: FlagSnapshot;
children: ReactNode;
}) {
const value = useMemo(() => snapshot, [snapshot]);
return <FlagsContext.Provider value={value}>{children}</FlagsContext.Provider>;
}
export function useFeatureFlag(key: string): boolean {
const flags = useContext(FlagsContext);
return Boolean(flags[key]);
}// Server layout — bootstrap flags into HTML
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const snapshot = await getFlagsForUser();
return (
<html>
<body>
<FlagsProvider snapshot={snapshot}>{children}</FlagsProvider>
</body>
</html>
);
}- Bootstrap — flags available before first render (SSR inline or sync cache)
- Snapshot model — one payload, local O(1) reads
- Stable bucketing — hash(userId + flagKey) for consistent A/B assignment
- Refresh — polling, SSE, or refetch on navigation
Assign deviceId cookie for anonymous users; merge to userId on login without changing variant when possible.
Interview: Local evaluation against a rule snapshot scales — one download, not server round-trip per flag check.
Build-Time vs Runtime Flags
Definition
| Build-time | Runtime | |
|---|---|---|
| Flip without deploy | No | Yes |
| Remove dead code | Yes | No (usually) |
| User targeting | No | Yes |
| A/B experiments | No | Yes |
// Build-time — inlined at compile
const ANALYTICS_DEBUG = process.env.NEXT_PUBLIC_ANALYTICS_DEBUG === 'true';
// Runtime — fetched after load
async function loadRuntimeFlags() {
const res = await fetch('/api/flags');
return res.json();
}const NewDashboard = process.env.NEXT_PUBLIC_NEW_DASHBOARD === 'true'
? lazy(() => import('./NewDashboard'))
: null;Build-time bakes into bundle — tree-shaking can remove dead branches. Good for environment config and flags that never flip without deploy.
Runtime toggles instantly, targets users, runs experiments. NEXT_PUBLIC_FEATURE_X=false still ships feature code unless bundler eliminates the branch.
Never put secrets in NEXT_PUBLIC_* — visible in every client bundle. Use dynamic import() when runtime flag hides UI but bundle size matters.
Next.js middleware can fetch flags for route gating before HTML streams.
Interview: Runtime hides UI; build-time can remove bytes. Document which layer wins per flag.
Preventing Flash of Wrong Content
Definition
FOWC — user briefly sees wrong variant before flag resolves. Fix by bootstrapping flags before paint.
// Server — inline snapshot in HTML
<script
dangerouslySetInnerHTML={{
__html: `window.__FLAGS__=${JSON.stringify(snapshot)}`,
}}
/>
// Client — read synchronously on first render
const initialFlags = typeof window !== 'undefined' ? window.__FLAGS__ : {};Strategies:
- SSR inline snapshot — flags in HTML before hydration
- Blocking script in
<head>for critical gates (use sparingly) - Default to safe/off until snapshot loads — hide gated UI, show skeleton
- CSS
visibility: hiddenon gated regions until flags ready — avoid layout shift
Experiments on above-the-fold hero content need server-side evaluation — client-only fetch causes visible swap.
For entitlement flags (paid features), server must reject API calls — client hide is not enough.
Interview: FOWC on hero content requires server-evaluated snapshot, not client fetch after paint.
Flag Architecture at Scale
Definition
Staff concerns at scale:
- Single source of truth — flag registry with owner, type, sunset date
- Consistent evaluation — same bucketing key server and client
- Audit trail — who flipped what, when (LaunchDarkly, Unleash, Flagsmith)
- Kill switches — ops flags with paging integration
- Percentage rollouts — stable hash per user, not per request
Flag lifecycle:
create → dark launch → % rollout → 100% → remove flag + dead codeRequired changeset-style review for production flag changes in regulated environments.
Integration tests wrap FlagsProvider with test snapshot — no network. E2E hits mock flag API.
Per-flag metrics: exposure count, conversion by variant, error rate delta. Auto-rollback when error rate spikes.
Avoid 200 ad-hoc if (flag) branches — centralize evaluation, typed flag keys, codegen from registry.
Interview: Schedule flag cleanup — stale flags are production debugging debt.
Senior Interview Summary
Flag types
Release, experiment, ops, entitlement — client gates UX; server enforces auth.
Bootstrap
Snapshot before first render. Provider + hook. Stable bucketing hash.
Build vs runtime
Build-time removes dead code. Runtime enables targeting without redeploy.
FOWC
SSR inline snapshot. Default safe/off. Server eval for above-the-fold experiments.
Scale
Registry, audit, sunset dates, kill switches, metrics per variant.
Best mental model
Deploy ≠ release
Client = UX gate
Server = security + authoritative eval for entitlementsPractical rules
Every flag → owner + default + sunset date
Experiments → stable user key, not per-request random
Entitlements → server must reject, not just hide UI
Cleanup → remove flag + dead code after 100% rolloutWhat to say in interviews
Lead with deploy vs release, explain bootstrap snapshot pattern, compare build-time vs runtime with a table, and describe FOWC prevention for hero content. Mention flag cleanup discipline.