Feature Flags at Scale: Harnessing Unleash in a Next.js Application
Introduction
Feature flags (also called feature toggles) let you ship code first and decide later whether a new capability is visible to users. When used responsibly they reduce release risk, enable canary roll‑outs, and empower product teams to experiment without multiple deployments.
At a small team a single JSON file can be enough, but once you start serving thousands of requests per second, multiple environments, and granular targeting (e.g., “beta users in Europe”), you need a robust flag‑management platform. This article shows how to adopt Unleash, an open‑source feature‑flag service, in a Next.js codebase that runs both on the server (Node) and the client (React). We’ll cover architecture, practical integration steps, rollout patterns, and operational tips for keeping the system performant and maintainable.
TL;DR – By the end of this article you’ll have a working Next.js project that reads flags from an Unleash server, supports server‑side rendering (SSR) and static generation, and can be safely rolled out to production with gradual exposure.
1. Why a Dedicated Flag Service?
| Problem | Ad‑hoc solution | Dedicated service (Unleash) |
|---|---|---|
| Consistency across services | Each repo keeps its own flag file → drift | Single source of truth |
| Targeting (user, region, version) | Hard‑coded if statements |
Built‑in strategies |
| Audit & rollback | Git history only | UI + API for enable/disable, audit logs |
| Performance | File I/O on every request | In‑memory cache + SSE updates |
| Scalability | Manual sync scripts | Horizontal scaling, clustering |
When you start to need any of the above, a purpose‑built system pays for itself.
2. Unleash Overview
Unleash is a feature‑toggle SaaS (hosted) and self‑hosted solution written in Java‑Kotlin. Its core concepts:
| Concept | Meaning |
|---|---|
| Feature | A named toggle (e.g., new-dashboard) |
| Strategy | Rules that decide if a flag is active for a request (e.g., userId, gradualRollout, default) |
| Context | Data you send with each request (userId, environment, sessionId, ip) |
| Client SDK | Lightweight libraries for Node, JavaScript, Go, etc. They keep a local cache and receive updates via Server‑Sent Events (SSE). |
Unleash’s SSE model means the client only contacts the server once per process, then receives incremental updates. This is perfect for Next.js where the Node server (or Vercel edge runtime) can keep a long‑lived connection.
3. Project Layout
my-next-app/
├─ pages/
│ ├─ index.tsx
│ └─ admin/
│ └─ feature-flags.tsx
├─ lib/
│ ├─ unleashClient.ts # Node SDK wrapper
│ └─ unleashReact.ts # React hook wrapper
├─ types/
│ └─ unleash.d.ts # Type definitions for flag payloads
└─ next.config.js
unleashClient.ts– server‑side singleton that talks to Unleash.unleashReact.ts– React hook that reads the same cache on the client.feature-flags.tsx– an internal admin page that demonstrates live toggling.
4. Installing the SDKs
# Server side (Node)
npm i unleash-client
# Client side (React)
npm i unleash-client @unleash/proxy-client-react
Note – The proxy client is recommended for browsers because it never exposes the API token. Instead you run a lightweight Unleash Proxy (Docker image) that forwards requests with a public token.
5. Server‑Side Integration
5.1 Create a Singleton
// lib/unleashClient.ts
import { Unleash } from 'unleash-client';
let unleash: Unleash | null = null;
export function getUnleashInstance() {
if (unleash) return unleash;
unleash = new Unleash({
url: process.env.UNLEASH_URL!, // e.g. https://unleash.mycompany.com/api/
appName: 'nextjs-frontend',
instanceId: `next-${process.pid}`,
customHeaders: {
Authorization: process.env.UNLEASH_API_TOKEN!,
},
refreshInterval: 15_000, // 15 s cache refresh
metricsInterval: 60_000, // send usage metrics
});
// Wait for the SDK to be ready before using it
unleash.on('ready', () => console.log('🔓 Unleash ready'));
unleash.on('error', (err) => console.error('Unleash error', err));
return unleash;
}
The singleton ensures the SDK is instantiated once per server process, keeping the SSE connection alive.
5.2 Using Flags in getServerSideProps
// pages/index.tsx
import { GetServerSideProps } from 'next';
import { getUnleashInstance } from '../lib/unleashClient';
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const unleash = getUnleashInstance();
// Build the context that Unleash expects
const userId = ctx.req.cookies['uid'] ?? 'anonymous';
const context = { userId, environment: process.env.NODE_ENV };
const showNewDashboard = unleash.isEnabled('new-dashboard', context);
const maxItems = unleash.getVariant('dashboard-item-limit', context).name; // e.g., "10", "20"
return {
props: {
showNewDashboard,
maxItems: Number(maxItems) || 5,
},
};
};
export default function Home({ showNewDashboard, maxItems }: { showNewDashboard: boolean; maxItems: number }) {
return (
<main>
<h1>Welcome</h1>
{showNewDashboard ? (
<section>
<h2>🚀 New Dashboard</h2>
<p>Showing up to {maxItems} items.</p>
</section>
) : (
<section>
<h2>Classic Dashboard</h2>
</section>
)}
</main>
);
}
Key points
- Context is built from request data (cookies, headers).
isEnabledreturns a boolean;getVariantsupports A/B testing.- Because the flag check runs before rendering, the HTML sent to the browser already reflects the correct version – great for SEO and performance.
6. Client‑Side Integration
6.1 Set Up the Proxy
Run the proxy locally for development:
docker run -d \
-p 8080:80 \
-e UNLEASH_URL=https://unleash.mycompany.com/api/ \
-e UNLEASH_PROXY_CLIENT_ACCESS_TOKEN=public-token \
unleashorg/unleash-proxy
The proxy exposes /api/client/features which the browser SDK consumes.
6.2 React Hook Wrapper
// lib/unleashReact.ts
import { createContext, useContext, ReactNode } from 'react';
import { UnleashClient, createInstance } from '@unleash/proxy-client-react';
const unleash = createInstance({
url: process.env.NEXT_PUBLIC_UNLEASH_PROXY_URL!, // e.g. http://localhost:8080/api/
clientKey: process.env.NEXT_PUBLIC_UNLEASH_PROXY_TOKEN!,
refreshInterval: 10_000,
});
export const UnleashContext = createContext<UnleashClient>(unleash);
export const UnleashProvider = ({ children }: { children: ReactNode }) => (
<UnleashContext.Provider value={unleash}>{children}</UnleashContext.Provider>
);
export const useFlag = (name: string, fallback = false) => {
const client = useContext(UnleashContext);
return client.isEnabled(name, fallback);
};
export const useVariant = (name: string, fallback = { name: 'disabled' }) => {
const client = useContext(UnleashContext);
return client.getVariant(name, fallback);
};
Wrap your app in _app.tsx:
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { UnleashProvider } from '../lib/unleashReact';
function MyApp({ Component, pageProps }: AppProps) {
return (
<UnleashProvider>
<Component {...pageProps} />
</UnleashProvider>
);
}
export default MyApp;
6.3 Using the Hook in a Component
// components/PromoBanner.tsx
import { useFlag } from '../lib/unleashReact';
export default function PromoBanner() {
const showPromo = useFlag('promo-banner', false);
if (!showPromo) return null;
return (
<aside className="promo">
🎉 Limited‑time offer! Get 20 % off your first month.
</aside>
);
}
Because the proxy client caches flags locally and updates via SSE, the banner appears instantly after a flag is turned on, without a page reload.
7. Rollout Strategies
Unleash ships several built‑in strategies that you can combine:
| Strategy | Typical use case |
|---|---|
| default | Simple on/off for all users |
| userId | Enable for a whitelist of user IDs |
| gradualRollout | Percentage‑based rollout (e.g., 10 % of traffic) |
| remoteAddress | Target by IP range or country (via GeoIP) |
| custom | Write a server‑side function for complex logic |
Example: Gradual Rollout with a “Beta” Group
- Create a feature
new-searchin the Unleash UI. - Add a gradualRollout strategy with
percentage: 5. - Add a userId strategy with a list of internal testers.
When you query the flag, Unleash evaluates strategies in order. Internal testers see the feature immediately; the rest of the world gets it only when the random bucket falls within the 5 % slice.
8. Testing Flags Locally
8.1 Mocking the Server SDK
During unit tests you don’t want to spin up an Unleash server. The Node SDK exposes a toggle method that lets you override flag values:
// test/setupUnleash.ts
import { getUnleashInstance } from '../lib/unleashClient';
export const mockFlag = (name: string, enabled: boolean) => {
const unleash = getUnleashInstance();
// @ts-ignore – internal API for testing
unleash.toggle(name, enabled);
};
Use it in a Jest test:
import { render, screen } from '@testing-library/react';
import Home from '../pages/index';
import { mockFlag } from '../test/setupUnleash';
test('shows new dashboard when flag enabled', async () => {
mockFlag('new-dashboard', true);
render(<Home showNewDashboard={true} maxItems={10} />);
expect(screen.getByText(/New Dashboard/)).toBeInTheDocument();
});
8.2 Mocking the Browser SDK
For React component tests, replace the UnleashProvider with a mock that returns a deterministic client:
// __mocks__/unleashReact.ts
import React, { createContext } from 'react';
export const UnleashContext = createContext({
isEnabled: (name: string) => name === 'promo-banner',
getVariant: () => ({ name: 'disabled' }),
});
export const UnleashProvider = ({ children }: any) => <>{children}</>;
export const useFlag = (name: string, fallback = false) => name === 'promo-banner';
export const useVariant = () => ({ name: 'disabled' });
Jest will automatically use this mock when you import from ../lib/unleashReact.
9. Performance & Caching Considerations
| Concern | Mitigation |
|---|---|
| Cold start latency (first request after server restart) | Pre‑warm the SDK in server.js by calling isEnabled for a known flag during startup. |
| Memory usage (large flag set) | Unleash stores flags in a plain object; keep the flag count reasonable (< 5 k). Use namespaces to group related flags. |
| SSR vs. static generation | For static pages (getStaticProps) you can embed a snapshot of flags at build time. If you need per‑user targeting, stick to getServerSideProps. |
| Network overhead | The SSE connection is a single long‑lived HTTP request per process. Ensure your hosting environment allows idle connections (e.g., Vercel Edge Functions keep connections alive for up to 30 s). |
| Client bundle size | The proxy client is ~5 KB gzipped. Load it lazily if you only need flags in a few components. |
10. CI/CD Integration
- Feature flag definition as code – Unleash supports importing/exporting JSON. Store the flag definition file (
unleash-flags.json) in your repo. - Automated validation – In a GitHub Actions workflow, run a script that checks the JSON against a schema (e.g., using
ajv). - Gate deployments – Use the flag state to decide whether a deployment should proceed. Example: a GitHub Action step that aborts if
critical-bug-fixflag is still disabled.
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate flags
run: node scripts/validate-flags.js
- name: Deploy to Vercel
if: success()
run: vercel --prod
11. Common Pitfalls & How to Avoid Them
| Pitfall | Symptom | Remedy |
|---|---|---|
| Flag leakage to the client | Sensitive business logic appears in the browser bundle. | Keep security‑critical decisions on the server side (use Node SDK). Only expose UI toggles via the proxy. |
| Stale cache after rollout | Users see old UI for minutes after flag is turned on. | Verify the SSE connection is alive; increase refreshInterval if you use polling fallback. |
| Over‑targeting | Too many strategies make the flag hard to reason about. | Document each strategy in the Unleash UI and keep a “owner” label. |
| Feature flag debt | Old flags never removed, cluttering the UI. | Adopt a flag retirement policy: after 2 weeks of stable operation, delete the flag and clean up code. |
| Testing blind spots | Tests pass locally but fail in production because the flag state differs. | Include a “test” environment in Unleash and lock its flag set to a known snapshot. |
12. Real‑World Example: A Multi‑Region Beta
Scenario: A SaaS product wants to roll out a new analytics dashboard to beta users in Europe first, then gradually expand to North America.
Steps
- Create feature
analytics-dashboardin Unleash. - Add two strategies:
remoteAddress→country: EUwithpercentage: 30.userId→ whitelist of internal testers.
- In
getServerSideProps, pass the request IP to the context (remoteAddress). - In the React component, use
useFlag('analytics-dashboard')to decide which component tree to render. - Deploy the change to production behind the flag.
- Monitor usage via Unleash’s built‑in metrics (hits per flag). When the error rate is low, increase the percentage to 70, then 100.
This pattern demonstrates gradual rollout, regional targeting, and real‑time feedback without a new deployment.
13. Summary
Feature flags are a powerful lever for continuous delivery, but they become a liability if managed ad‑hoc. By pairing Unleash—a battle‑tested, open‑source flag service—with Next.js’s flexible data‑fetching methods, you get:
- Single source of truth for all environments.
- Zero‑downtime rollouts using built‑in strategies.
- SSR‑compatible checks that keep SEO and performance intact.
- Client‑side toggles via the lightweight proxy SDK, without exposing secret tokens.
- Observability through built‑in metrics and audit logs.
Implement the steps outlined above, adopt a flag‑retirement cadence, and you’ll be able to ship changes confidently.
Member discussion