Skip to main content
btheo.com btheo.com > press start to play
NEW POST: NODE.JS SECURITY 2025 OPEN FOR FREELANCE 10+ YEARS EXP REACT × NODE × AWS NEW POST: NODE.JS SECURITY 2025 OPEN FOR FREELANCE 10+ YEARS EXP REACT × NODE × AWS
NODE.JS 4 MIN READ

Feature Flags in Production: Ship Without Fear

WARNING · DRAGON AHEAD

Your deploy is done. But the feature isn’t ready. You wait. Days pass. Merge conflicts multiply. A long-running branch becomes a merge disaster.

Feature flags solve this. Deploy code dark. Turn it on when you’re ready. Ship every day. Release once a week.

The Four Types of Flags

🔹 Release flags: Feature incomplete? Ship it off. if (flags.newCheckout) { ... }
🔹 Experiment/A-B flags: 50% of users see variant A, 50% see B. Measure conversion.
🔹 Ops flags: Kill switches. If production melts, flip a flag. No deploy needed.
🔹 Permission flags: Beta testers, internal teams. if (flags.betaFeatures && user.isBeta) { ... }

Most teams need 2-3 types. Don’t over-engineer.

DIY: Redis + JSON

The simplest production setup: store flag config as JSON in Redis, evaluate per request.

Flag schema:

type FlagConfig = {
[key: string]: {
enabled: boolean;
rollout?: number; // 0-100, percentage of users
userIds?: string[]; // Specific user overrides
environments?: string[]; // Only prod, only staging, etc
}
};

Redis storage:

const flagConfig = {
"newCheckout": {
enabled: true,
rollout: 25, // 25% of users
environments: ["production"]
},
"darkMode": {
enabled: true,
rollout: 100,
userIds: ["user-123"] // Force on for this user
}
};
await redis.set("feature-flags", JSON.stringify(flagConfig));

Evaluation logic:

async function isEnabled(
flagName: string,
userId: string,
environment: string
): Promise<boolean> {
const config = JSON.parse(await redis.get("feature-flags"));
const flag = config[flagName];
if (!flag || !flag.enabled) return false;
if (flag.environments && !flag.environments.includes(environment)) {
return false;
}
if (flag.userIds?.includes(userId)) return true;
if (flag.rollout) {
const hash = hashUserId(userId);
return (hash % 100) < flag.rollout;
}
return true;
}

Hash the user ID consistently so they always see the same variant.

function hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = ((hash << 5) - hash) + userId.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash);
}

Express Middleware: Attach Flags to Request

app.use(async (req, res, next) => {
const userId = req.user?.id || "anonymous";
const env = process.env.ENVIRONMENT || "development";
req.flags = {
newCheckout: await isEnabled("newCheckout", userId, env),
darkMode: await isEnabled("darkMode", userId, env)
};
next();
});
app.get("/checkout", (req, res) => {
if (req.flags.newCheckout) {
return res.json(checkoutV2());
}
return res.json(checkoutV1());
});

React: Context Provider

Fetch flags once on app load, provide via context.

type FlagsContextType = {
newCheckout: boolean;
darkMode: boolean;
isLoading: boolean;
};
const FlagsContext = createContext<FlagsContextType>({
newCheckout: false,
darkMode: false,
isLoading: true
});
export function FlagsProvider({ children }: { children: React.ReactNode }) {
const [flags, setFlags] = useState<FlagsContextType>({
newCheckout: false,
darkMode: false,
isLoading: true
});
useEffect(() => {
fetch("/api/flags")
.then(r => r.json())
.then(data => setFlags({ ...data, isLoading: false }))
.catch(() => setFlags(f => ({ ...f, isLoading: false })));
}, []);
return (
<FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
);
}
export function useFlags() {
return useContext(FlagsContext);
}

In a component:

export function Checkout() {
const { newCheckout } = useFlags();
return newCheckout ? <CheckoutV2 /> : <CheckoutV1 />;
}

OpenFeature Standard

Don’t want to DIY? OpenFeature is a vendor-neutral SDK for feature flags. Works with any backend.

import { OpenFeature } from "@openfeature/web-sdk";
const provider = new YourFlagProvider();
OpenFeature.setProvider(provider);
const client = OpenFeature.getClient();
const isEnabled = client.getBooleanValue("newCheckout", false);

Works with LaunchDarkly, Unleash, GrowthBook, Firebase, or your custom Redis backend.

GrowthBook: Self-Hosted Option

Want a UI for managing flags without building one? GrowthBook is open-source, self-hosted, and cheap.

import { GrowthBook } from "@growthbook/sdk-js";
const gb = new GrowthBook({
apiHost: "https://growthbook.internal.com",
clientKey: "sdk_prod_xxx"
});
await gb.loadFeatures();
if (gb.isOn("newCheckout")) {
// Show new checkout
}

You get a dashboard to toggle flags, run A/B tests, see rollout %, all without touching code.

Flag Hygiene: Don’t Let Them Rot

Flags you forgot about become dead code. Three months later, half your conditional branches are unreachable.

Quarterly audit:

  • Find all flags in use: grep -r "isEnabled\|flags\." src/
  • Check which are 100% enabled
  • Remove them: if (true) { ... } → just the branch
  • Mark for deletion: // TODO: remove newCheckout flag by 2026-03-01

Dead flags aren’t just cruft. They’re performance leaks (every request checks them) and confusion for new developers.

Testing With Flags

Test both paths. A feature behind a flag is still a feature.

describe("Checkout with newCheckout flag", () => {
it("shows new checkout when enabled", async () => {
await redis.set("feature-flags", JSON.stringify({
newCheckout: { enabled: true, rollout: 100 }
}));
const res = await request(app).get("/checkout");
expect(res.body.version).toBe("v2");
});
it("shows old checkout when disabled", async () => {
await redis.set("feature-flags", JSON.stringify({
newCheckout: { enabled: false }
}));
const res = await request(app).get("/checkout");
expect(res.body.version).toBe("v1");
});
});

The Real Win

Feature flags decouple deploy (push code) from release (turn it on). This is everything.

Deploy 10 times a day. Release once. If something breaks, flags revert instantly. No rollback. No downtime. No Friday night pages.

That’s production velocity.

ALL POSTS →