IAnswerable

Feature Flags Without Paying for a Vendor

06 Feb 2026

LaunchDarkly 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

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.