Back to Lab
RAXXO Studios 9 min read No time? Make it a 1 min read

Feature Flags Without a Vendor: The Minimal Setup I Ship

Architecture
9 min read
TLDR
×
  • Boolean flags in env vars cover 80% of what vendors sell
  • Gradual rollout by hashing user IDs into 100 buckets
  • Instant rollback in under 60 seconds with no deploy
  • Full kill-switch table lives in one edge config file

I ship products alone. When I priced a feature flag vendor last year, the smallest paid tier wanted 40 EUR a month for things I already had access to for free. So I built my own setup in an afternoon. It has run three products through dozens of releases without a single incident I could blame on it.

Here is the whole thing: environment variables, one edge config file, and a small hash function. No SDK, no dashboard, no vendor.

What a Flag Vendor Actually Sells You

Before you copy anyone's setup, it helps to name what you are actually paying for. Feature flag vendors bundle five things into one bill. First, a place to store on/off switches. Second, a way to flip them without redeploying code. Third, targeting rules (show this to 10% of users, or only users in Germany). Fourth, an audit log of who changed what. Fifth, a fancy dashboard.

For a team of thirty engineers pushing forty times a day, that bundle is worth real money. For a solo product, most of it is overhead you will never touch. I have never once needed to know "who changed the flag" because the answer is always me.

So I reduced the list to what I genuinely use. I need switches. I need to flip them without a deploy. I need to roll a feature out to a slice of users and widen it slowly. That is three things, not five. And all three exist inside tools I already pay for as part of hosting.

The trap is thinking flags are complicated because vendors have made a whole category out of them. A boolean is not complicated. A boolean stored somewhere your app can read at request time is not complicated either. The only genuinely tricky part is gradual rollout, and even that is one function.

I keep a running rule for solo tooling: if a service charges monthly for something I can express in under 50 lines of code, I write the 50 lines. Flags cleared that bar easily. The 40 EUR a month I did not spend is 480 EUR a year, and the setup has needed zero maintenance since I shipped it.

If you want the wider thinking on building instead of buying, Claude Blueprint walks through how I decide what to keep in-house versus outsource. Flags land firmly in the keep column.

Boolean Flags Live in Env Vars

The simplest flags are the ones you never plan to change while the app is running. Is the new checkout live? Is the beta search bar visible? These are settings, and settings belong in environment variables.

I name them with a strict prefix so they are easy to scan. Every flag starts with `FLAG_`. So I have `FLAG_NEW_CHECKOUT`, `FLAG_BETA_SEARCH`, `FLAG_AI_SUGGESTIONS`. The value is the string "on" or "off". I read them through one helper that treats anything except "on" as off, so a typo fails safe instead of accidentally enabling something.


function flag(name) {
  return process.env[`FLAG_${name}`] === "on";
}

That is the entire read path for static flags. In the code I write `if (flag("NEW_CHECKOUT"))` and move on. There is no SDK to initialize, no network call, no latency. The value is baked in at boot.

The catch with env vars is that changing one usually means a redeploy. On most hosts, editing an environment variable triggers a rebuild that takes two to five minutes. That is fine for flags I flip a few times during a launch. It is not fine for an emergency where a feature is actively breaking and I need it off in seconds. That case needs the edge config, which I cover next.

I keep a plain text list of every active flag at the top of one file, with a one-line note on what each controls and when I can delete it. Flags rot fast. A flag that has been "on" for everyone for two months is not a flag anymore, it is dead code wrapped in a condition. Every few weeks I delete the ones that graduated and clean up the branches. This single habit has kept my codebase from turning into a maze of nested toggles, which is the real long-term cost people forget when they adopt flags at all.

For the deployment side of this, I run everything on Shopify hosting for the storefront pieces and a small edge platform for the app logic, so my env vars sit in two clearly separated places.

Instant Rollback With an Edge Config Table

The env var approach fails the one test that matters most: the 3am kill switch. When something is on fire, waiting five minutes for a rebuild is unacceptable. So the flags I might need to flip fast do not live in env vars at all. They live in an edge config store.

Most edge platforms give you a tiny key-value store that reads in single-digit milliseconds at the edge and updates in seconds with no deploy. Vercel calls it Edge Config. Cloudflare has KV. The idea is the same everywhere: a small JSON blob your app reads on each request without a rebuild.

My kill-switch table is one JSON object:


{
  "new_checkout": { "enabled": true, "rollout": 100 },
  "ai_suggestions": { "enabled": true, "rollout": 25 },
  "beta_search": { "enabled": false, "rollout": 0 }
}

To kill a feature I set `enabled` to false and save. The change is live at the edge in under 60 seconds. No deploy, no rebuild, no waiting. I have used this exactly twice in production, and both times the difference between 60 seconds and five minutes was the difference between a shrug and a bad morning.

The read helper checks the edge table first and treats a missing key as disabled, so again the failure mode is safe. If the edge store is unreachable for any reason, every gated feature turns off rather than throwing errors at users. Off is almost always safer than broken.

I keep the whole table in version control as a committed default too. That way I always have a record of the intended state, and if I ever migrate platforms I can seed the new store from the file. This gives me the audit trail a vendor would sell me, without paying for it. The git history shows every change, timestamped, with a commit message I can make as descriptive as I want.

One rule I follow: the edge table only holds flags I might flip in an emergency. Everything else stays in env vars. Mixing them makes the table cluttered and slow to reason about at 3am, which is the exact moment I need it to be obvious.

Gradual Rollout With a Hash Function

The last piece vendors charge for is percentage rollout. Show a feature to 10% of users, watch the error logs, then widen to 50%, then 100%. This sounds like it needs a targeting engine. It needs one function.

The problem to solve is stickiness. If I roll out to 10% by flipping a coin on each request, a user sees the feature on one page and gone on the next. That is worse than not shipping. So the assignment has to be deterministic per user: the same user always lands in the same bucket.

I hash the user ID into a number from 0 to 99, then compare it against the rollout percentage.


function inRollout(userId, percent) {
  let h = 0;
  for (const c of userId) h = (h * 31 + c.charCodeAt(0)) % 100;
  return h < percent;
}

A user hashed to bucket 7 is inside any rollout of 8% or higher, and always will be, because the hash is stable. When I widen from 10% to 25%, the original 10% keep the feature and a fresh 15% join them. Nobody loses access mid-rollout. That property, stable assignment as you widen, is the whole game, and it is four lines.

I combine this with the edge table. A flag is live for a user when `enabled` is true AND `inRollout(userId, rollout)` returns true. So I can ship at `rollout: 5`, watch logs for an hour, bump to 25, then 50, then 100, all by editing one number in the edge store. No deploy at any step.

For features I want to test more precisely, I log which bucket each user landed in alongside the metric I care about. That gives me a rough before/after without any analytics vendor. I ran my AI suggestions feature this way: 5% for a day, checked that error counts stayed flat and engagement held, then widened over four days to everyone. If the numbers had gone the wrong way I would have set `rollout` back to 0 in seconds.

For the AI features themselves I lean on tools like ElevenLabs for voice and Magnific for image work, and gating each new integration behind a slow rollout has saved me from shipping a broken third-party dependency to my whole userbase more than once.

Bottom Line

Feature flags are three small pieces, not a subscription. Boolean switches live in env vars for anything I change slowly. A tiny JSON table in edge config handles the emergency kill switch with rollback in under 60 seconds and no deploy. A four-line hash function gives me sticky percentage rollout that widens without ever revoking access. That covers what I used to price a vendor for at 40 EUR a month.

The real cost of flags is never the storage. It is the dead flags that pile up and turn your code into a maze. So the discipline that matters is deletion: retire every flag the moment it has graduated to permanent. I do a cleanup pass every few weeks and it keeps the whole thing honest.

If you are building solo and weighing what to keep in-house, this is a clear win. Start with the env var helper, add the edge table only for the flags you might panic-flip, and reach for the hash function when you want a slow rollout. For more on how I decide build versus buy across a whole product, Claude Blueprint has the full framework.

This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)

Stay in the loop
New tools, drops, and AI experiments. No spam. Unsubscribe anytime.
Back to all articles