Feature Flags Without Paying for a Vendor
06 Feb 2026LaunchDarkly is a real product with a real reason to exist — if you need percentage rollouts across a distributed org, targeting rules by attribute, and an audit trail, buy it. If you need “turn this thing on for a few users without deploying,” you can build that in an afternoon.
Two kinds of flags, two mechanisms
Global flags (on/off for everyone, changed rarely): environment variables. You already have a deploy mechanism; use it.
FEATURE_NEW_CHECKOUT=true
const flags = {
newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true',
}
if (flags.newCheckout) {
// new path
}
No database, no service, no library. Flipping it means changing an env var and restarting the app — which is a deploy, but a fast one, and it’s auditable through your normal deploy history.
Per-user or percentage flags (roll out to specific accounts, or 10% of traffic): a config table plus an in-process cache.
CREATE TABLE feature_flags (
key TEXT PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT false,
rollout_percent INTEGER NOT NULL DEFAULT 0,
enabled_user_ids INTEGER[] DEFAULT '{}'
);
let flagCache = new Map()
let lastFetch = 0
async function getFlags() {
if (Date.now() - lastFetch > 30_000) {
const rows = await db.query('SELECT * FROM feature_flags')
flagCache = new Map(rows.map(r => [r.key, r]))
lastFetch = Date.now()
}
return flagCache
}
async function isEnabled(key, userId) {
const flags = await getFlags()
const flag = flags.get(key)
if (!flag) return false
if (flag.enabled) return true
if (flag.enabled_user_ids.includes(userId)) return true
if (flag.rollout_percent > 0) {
// stable hash so the same user always lands on the same side
const hash = simpleHash(`${key}:${userId}`) % 100
return hash < flag.rollout_percent
}
return false
}
The 30-second in-memory cache matters — without it, every request checking a flag is a database round trip, and you’ve turned a boolean check into a query on your hot path.
The hash needs to be stable
Don’t use Math.random() to decide rollout membership. That flips a user in and out of the treatment group on every request, which makes bugs impossible to reproduce and breaks any experiment analysis you try to run on top of it. Hash on key + userId so a given user’s bucket is deterministic across requests.
function simpleHash(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i)
hash |= 0
}
return Math.abs(hash)
}
An admin toggle, without an admin panel
You don’t need a UI to flip flags — a script that hits the database directly is fine at small scale:
psql $DATABASE_URL -c \
"UPDATE feature_flags SET rollout_percent = 25 WHERE key = 'new_checkout';"
Add a thin internal route behind auth once more than one person needs to flip flags without shell access to the database.
Where this stops being enough
- You need flags to target by attribute (plan tier, signup date, region) instead of just user ID — that’s a real targeting rules engine, and building it yourself starts costing more than it saves.
- You need flag changes audited with who-changed-what-when for compliance.
- You want flag-driven A/B test analysis with statistical significance built in, not just a boolean split.
At that point, pay for the vendor — you’re not saving engineering time anymore, you’re spending it rebuilding their product badly.
Takeaway
Env vars for global toggles, one table plus a cached read for per-user rollout. Both together are maybe 40 lines of code and no new service to operate, monitor, or pay for.