Flaggy
Get started
← Blog

JavaScript feature flags: a complete guide

How JavaScript feature flags work across the browser, Node, SSR, and edge — where to evaluate, how to avoid hydration flicker, and when to reach for an SDK.

A JavaScript feature flag is a runtime switch around a branch of code:

if (flags.isEnabled('release-new-checkout')) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

The interesting part isn’t the if — it’s that the answer is controlled from a dashboard at runtime, not hardcoded at deploy time. You ship both paths, then decide who sees which one, and when, without touching the code again.

What makes flags genuinely different in JavaScript is where the code runs. The same SDK call can execute in a browser tab, a Node server, a server-rendered page, or an edge worker — and the right place to evaluate a flag changes with it. This guide is about that decision. If you’re new to the concept itself, start with what are feature flags and come back. For a straight step-by-step walkthrough, see how to use feature flags in JavaScript and TypeScript.

The mental model

Every flag SDK gives you one core function: pass a key and an optional user context, get back a boolean. Evaluation is a local, in-memory lookup against rules the client downloaded on startup — no network round-trip happens at the point of the check.

import { flaggy } from '@flaggy.io/sdk-js';

const flags = flaggy({ apiKey: process.env.FLAGGY_API_KEY! });
await flags.initialize();

flags.isEnabled('release-new-checkout', { key: user.id }); // → true | false

That local-evaluation model is the reason a single SDK can work everywhere JavaScript runs. The only question is where you call it.

Where your JavaScript runs — and why it matters

The same flag check behaves differently depending on the runtime:

  • Browser (SPA) — React, Vue, Svelte, plain JS. The flag decides what the user sees on the client.
  • Node — an API, a worker, a cron job. The flag gates server-side logic before anything reaches the browser.
  • SSR — Next.js, Remix, Astro, SvelteKit. Code runs twice: once on the server, once again when the page hydrates in the browser.
  • Edge / serverless — Cloudflare Workers, Vercel functions. Short-lived processes with cold starts.

These collapse into one decision: evaluate on the client, or on the server? Get that right and the rest is mechanical.

Client-side evaluation (the browser)

Create one shared client and reuse it everywhere — flaggy() returns a singleton, so calling it from multiple modules hands back the same instance.

// flags.ts — one shared client for the whole app
import { flaggy } from '@flaggy.io/sdk-js';

export const flags = flaggy({
  apiKey: import.meta.env.VITE_FLAGGY_API_KEY!,
  environment: import.meta.env.VITE_ENVIRONMENT, // "production" | "staging" | "development"
});

A client-side API key is safe to ship in the bundle — it only authorizes reading flag rules, never changing them.

Initialize once, before you render, so the first paint already has the right values and you avoid a flash of the wrong UI:

await flags.initialize();

In the browser, flags are cached in localStorage. If a warm cache exists, initialize() resolves immediately and kicks off a background refresh; on a cold first visit it waits for the initial fetch. After that, every isEnabled call is a synchronous lookup.

Server-side evaluation (Node)

The setup is the same; the lifecycle differs. A Node process caches flags in memory, and that cache is empty on every restart — so initialize() always waits for the first fetch. Run it before you accept traffic:

// server entry
import { flags } from './flags';

await flags.initialize();
app.listen(3000);
app.get('/checkout', (req, res) => {
  if (flags.isEnabled('release-new-checkout', { key: req.user.id, plan: req.user.plan })) {
    return res.render('checkout-v2');
  }
  return res.render('checkout-v1');
});

Server-side evaluation keeps your targeting rules off the client entirely and is the right call for anything sensitive — access control, pricing, gated APIs.

The SSR trap: hydration mismatches

This is the bug unique to JavaScript, and the one most guides skip. In an SSR framework your component renders on the server, ships as HTML, then re-renders on the client to attach event handlers (hydration). If a flag evaluates to true on the server but false on the client — because the client’s cache is cold, or it lacks the user context the server had — React throws a hydration mismatch and the UI flickers.

The fix is to decide once, on the server, and pass the result down as a prop. The client never re-evaluates:

// Next.js — evaluate on the server, hand the boolean to the page
export async function getServerSideProps({ req }) {
  const showNewUI = flags.isEnabled('release-new-ui', { key: req.user.id });
  return { props: { showNewUI } };
}

export default function Page({ showNewUI }) {
  return showNewUI ? <NewUI /> : <OldUI />;
}

One source of truth, no mismatch. For framework-specific patterns — providers, route gating, custom hooks — see the React and Angular guides.

How flag changes reach a running app

When you flip a flag in the dashboard, it does not reach users instantly. The SDK refreshes its cached rules by background polling — every 60 seconds by default, backing off on failure up to 15 minutes — so a change lands on clients at their next refresh, typically within about a minute. There’s no push or subscription event to listen for.

The honest selling point isn’t speed, it’s that the dashboard action needs no deploy. A product manager flips a switch; within a minute it’s live, with no build, no release, no engineer.

Because each isEnabled call reads the latest cached value, any component that re-renders after a refresh picks up the change automatically. For a long-lived view that must react mid-session — a kill switch firing, an experiment ending — poll on an interval instead of waiting for a natural re-render:

setInterval(() => {
  if (flags.isEnabled('kill-switch-payments')) disablePayments();
}, 60_000);

A/B tests and gradual rollouts

Flaggy’s flags are boolean — isEnabled returns true or false, not a variant string. You run experiments and canary releases with a percentage rollout: enable a flag for, say, 10% of users in the dashboard, and the SDK buckets each user deterministically by the key you pass, so the same user always lands on the same side. Watch your error rate, then widen the percentage.

flags.isEnabled('experiment-new-pricing-page', { key: user.id });

This covers the overwhelming majority of product experiments. If you need more than two paths, model them as separate boolean flags rather than one multivariate flag.

Build your own vs. an SDK

The simplest “feature flag” in JavaScript is an environment variable:

if (process.env.FEATURE_NEW_CHECKOUT === 'true') {
  // ...
}

That’s fine for a single static toggle. It falls apart the moment you want anything real:

  • Changing it needs a redeploy. The value is baked in at build time — the opposite of a runtime switch.
  • No per-user targeting. You can’t ship to enterprise accounts, a beta cohort, or 5% of traffic.
  • No audit trail. Nobody knows who changed what, or when.
  • Not safely available in the browser without inlining it into the bundle for every visitor.

An SDK gives you runtime control, segment-based targeting, and an audit log — the things the homegrown version can’t. Targeting rules live in the dashboard (“users where plan equals team”), so changing who sees a feature never touches your code.

Keeping JavaScript flags clean

The wiring is the easy part. The discipline is removing flags once a feature is fully rolled out, before your codebase fills with checks that are permanently true. A naming convention and a removal habit are what prevent that — covered in feature flag management.

Putting it together

One npm package covers both the browser and Node, evaluation is local in every runtime, and the same isEnabled call works whether you’re branching in a React component or a server route. The SDK reference has the full API.

Flaggy gives you local-evaluation SDKs, segmentation, flag analytics, and a complete audit log on a flat $99/month plan — unlimited seats, no per-user metering.