Topics
v6.0.0
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 typePurposeClient role
ReleaseHide incomplete featuresUX gating
ExperimentA/B variantsVariant UI + analytics
OpsKill-switch, maintenanceRoute/banner gating
EntitlementPlan tier, beta cohortMust 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 release

Client 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-timeRuntime
Flip without deployNoYes
Remove dead codeYesNo (usually)
User targetingNoYes
A/B experimentsNoYes
// 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: hidden on 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 code

Required 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 entitlements

Practical 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% rollout

What 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.

Last updated on