Novus Stream Solutions

2026 · Novus Stream Solutions (hub)About 12 min readNovus Stream Solutions

Feature flags without a platform

Feature flags are a technique, not a subscription. Before you pay a platform to toggle booleans, an environment variable, a typed config object, and twenty lines of hashing will carry a small product surprisingly far. Here is the ladder I climb, the cleanup discipline that keeps flags from rotting, and the honest point where a platform starts earning its fee.

A ladder of feature-flag techniques from an environment variable to a typed config module to a hash-based percentage rollout splitting traffic without an external platform
Contents
  1. 1.Overview
  2. 2.Rung one: the environment variable
  3. 3.Rung two: a typed flag module
  4. 4.Rung three: percentage rollout from a hash
  5. 5.Flags as an operations tool
  6. 6.Cleanup, or flags are debt
  7. 7.Testing both sides while the fork is live
  8. 8.When a platform earns its fee

Overview

A feature flag is an if statement whose condition lives outside the code path it protects. That is the entire technology. Somewhere along the way, this if statement acquired a product category, sales teams, and per-seat pricing, and small teams started believing they needed a vendor to ship code safely. I run a handful of browser tools alone; everything meaningful here has shipped behind a flag for two years, and no platform has ever been involved. Total infrastructure: one environment-variable namespace, one TypeScript module, and a hash function.

To be clear about what I am not arguing: flag platforms are real products that earn their keep in real situations, and the last section of this post names those situations honestly. What I am arguing is sequencing. The valuable thing is the practice — separating deploy from release, ramping changes gradually, keeping a kill switch on anything scary — and the practice is available for free, today, at a scale where a platform would be a subscription in search of a problem. Learn the practice on twenty lines of your own code; buy the product when you hit the wall the product exists for.

So this is a ladder with three rungs, ordered by what each additionally buys: the environment variable, the typed flag module, and the percentage rollout built on a hash. Then the two disciplines that matter more than any mechanism — flags as an operations tool, and cleanup, because a flag that outlives its decision is a small lie embedded in the codebase — and finally the checklist for when a platform starts being worth actual money.

Rung one: the environment variable

NEXT_PUBLIC_NEW_EXPORT_PIPELINE=false — or whatever your stack’s equivalent is — plus an if around the new code path. This is the flag most features need for their whole life. It separates the two events a small team most needs separated: the code landing in production (deploy) and the code running for users (release). The half-finished pipeline merges on Tuesday, dark; keeps merging all week, dark; and turns on the following Monday morning, when I am at the desk with coffee rather than at Friday dinner — the scheduling half of the reliability posture from Error budgets for tiny teams: reliability without an SRE org.

Environment variables also give you per-environment behavior essentially for free, because your host almost certainly scopes them already. The flag is on in local development, on in preview deployments, off in production, with no code expressing that anywhere — the pattern falls straight out of the deploy workflow in Shipping on Vercel solo: the deploy and preview workflow that keeps overhead near zero, where every branch gets a preview URL and the preview can simply run the future. I test a new pipeline for a week on previews with the exact bytes that will later serve production, which is a materially better guarantee than testing a special build.

Know the rung’s limits, because they are the reason the ladder continues. The env var is global — on for everyone or no one — and changing it takes a redeploy or restart, which can mean minutes of latency between "I want this off" and "it is off." For launch gating, that is nothing. For an emergency, minutes can matter, and for a gradual ramp, a global boolean is the wrong shape entirely. When either pressure appears, climb.

Rung two: a typed flag module

The second rung is not more power; it is more honesty. Scattered process.env reads rot into a codebase where nobody can answer "what flags exist and what state are they in?" without grepping for a naming convention someone half-followed. So: one flags.ts, exporting one typed object, where every flag has a name, a default, a comment saying what it protects, and — the field that changes everything — an expiry: a date or condition after which this flag should not exist. Call sites import the object; nothing else in the codebase touches raw flag state.

The compiler starts working for you immediately. A typo in a flag name is a build error instead of a silently-false condition — the same class of protection this site gets from typed content, where a broken reference fails the build instead of shipping a dead link. Deleting a flag from the module surfaces every call site as an error, which converts cleanup from an archaeology project into a fifteen-minute mechanical task. And the module doubles as the inventory: reading one file tells you the complete flag state of the system, which is the question every debugging session eventually asks.

This rung also establishes evaluation discipline: one function, evaluated at one layer, with results passed down — not sprinkled conditionals at five depths of the component tree. Sprinkled flags are how you get the bug where half the UI believes the feature is on and the other half disagrees, which is worse than either state. A flag evaluated once per session gives every component the same answer, makes the flagged surface testable as a unit, and means the eventual deletion is a single seam rather than a scavenger hunt.

Rung three: percentage rollout from a hash

The third rung is the one people assume requires a vendor, and it is twenty lines. The trick is stable bucketing: hash a stable identifier — an anonymous session id in our case, since these tools have no accounts — together with the flag’s name, take the result modulo 100, and the flag is on if the number falls below the rollout percentage. No network call, no service, no state to store. The same visitor lands in the same bucket every time, so the experience stays consistent, and including the flag name in the hash means the ten percent who see this experiment are not the same ten percent who see every experiment.

The ramp itself is a schedule with observation between steps: one percent overnight, ten percent for a day or two, fifty, one hundred. What you are buying at each step is bounded blast radius — at one percent, a defect that would have been an incident is a curiosity in the error console; at ten, a bad metric is visible before it is expensive. Each pause is only as good as the checking you actually do during it, so I pair every ramp step with the same watch-the-dashboard block that follows a deploy in A go-live runbook for a serverless browser app. A ramp without observation is just a slow global launch with extra steps.

Two implementation notes that save real pain. Use a decent string hash — FNV-1a is five lines and fine; the goal is distribution, not cryptography — because a lazy hash buckets unevenly and your "ten percent" is quietly four. And log the bucket decision alongside your existing analytics events so the two populations can be compared afterward; a rollout you cannot segment in the numbers taught you nothing except that nothing exploded, which is worth less than it feels. With accounts you would hash the user id instead; the pattern survives every identity scheme.

A pipeline from an environment variable through a typed flags module to a hash function that buckets sessions into a ninety percent control path and a ten percent rollout path
The whole stack: an env var for gating, one typed module as the inventory, and a five-line hash that turns sessions into stable rollout buckets.

Flags as an operations tool

The launch flag gets the attention, but the flag that has saved me actual money is the kill switch: a flag wrapped around a dependency I do not control, defaulting to on, existing so the dependency can be amputated in one move when it misbehaves. The canonical example in an ad-funded operation is the ad script itself. The day a network ships something that tanks page performance, the choice is between editing integration code under stress at midnight or flipping one switch and going back to bed. I wrap every third-party script, every external font, every remote anything in one.

Kill switches change incident math because they turn diagnose-then-fix into mitigate-then-diagnose. Without one, users bleed while you debug; with one, you flip the switch, the site degrades gracefully to its core function, and the debugging happens at whatever hour suits you. The discipline they demand is graceful absence: the page must be designed to render acceptably with the flagged thing gone, a one-time design cost paid at integration time. It is the same honesty principle as designing error states — assume the failure will happen and decide in advance what the user gets instead.

Operational flags are also how risky migrations become boring. Cutting over to a new model file, a new worker pipeline, a new storage schema — each ran here as a flag with the old path kept warm behind it, so the rollback story was never "revert the deploy and hope" but "flip back, instantly, with users mid-session." The test-and-release process in How we ship and test small apps without a full team leans on this constantly: the scary change ships dark, soaks on previews, ramps by percentage, and keeps its escape hatch until the new path has earned trust in production rather than in staging.

Cleanup, or flags are debt

Every flag is a fork in reality: two code paths, two behaviors, two things that can interact with every other live flag. Five flags is potentially thirty-two realities, most of which nobody has ever run. This is why flag debt is worse than most technical debt — dead code at least does nothing, while a stale flag keeps a dead branch executable, keeps its conditionals in every reader’s head, and waits for the day someone flips it without remembering what is behind it. The mechanism is genuinely dangerous without the discipline, which is why the discipline is not optional garnish; it is half the technique.

The rules I actually hold are few and blunt. Every flag is born with an expiry written into its definition, and the expiry is a promise, not a hint. The week a rollout reaches one hundred percent and holds, the losing branch dies — delete the flag from the module, follow the compiler errors, remove the old path, all while the context is still warm. A monthly skim of flags.ts asks one question of every survivor: what decision are you still waiting on? A flag that cannot name its pending decision is not a flag anymore; it is a setting nobody admitted to designing, and it either becomes a real documented setting or it dies.

The ceiling on simultaneous flags sounds arbitrary and is the most load-bearing rule of the set, because flag cost is combinatorial: each addition doubles the space of possible system states, and the marginal flag is always locally justified while the accumulation is what actually bites. A hard number forces the trade — shipping a sixth experiment means finishing one of the five — which is exactly the pressure that gets rollouts completed and branches deleted. Platforms, notably, have no incentive to hand you this pressure; their pricing pages celebrate flag counts the way airlines celebrate miles.

  • Every flag definition carries an expiry date or an explicit "permanent kill switch" designation — no third category exists.
  • A rollout that holds at 100% for a week means the losing code path gets deleted that same week, context still warm.
  • Monthly read of flags.ts: any flag that cannot name the decision it awaits is deleted or promoted to a real setting.
  • Hard ceiling on simultaneous non-permanent flags — mine is five — because combinations, not flags, are what multiply.
  • Kill switches are exempt from expiry but not from the audit: each must still name the dependency it amputates.

Testing both sides while the fork is live

An honest word about the tax: while a flag lives, you own two behaviors, and a test suite that only exercises one of them is signing checks the other might bounce. The practical protocol at this scale is not testing every combination — that road ends at five flags meaning thirty-two suites — but a priority order. The default state, the one real users are in, is tested continuously and fully; the incoming state is tested deliberately while it ramps; and the interaction cases are handled by the ceiling from the previous section rather than by heroics, because the honest answer to combinatorial state is less state, not more tests.

Kill switches get a special rule: rehearse the off state on a schedule, because a kill switch you have never flipped is a fire extinguisher you have never inspected. Once a quarter, previews run with each switch off, confirming the site still degrades the way the design promised — the fonts fall back, the ads leave a quiet gap instead of a broken frame, the core tool works untouched. Twice now that rehearsal has caught a regression where a "graceful" absence had silently become a layout collapse, which is precisely the discovery you want on a Tuesday preview rather than during the incident the switch exists for.

And the flag seams themselves earn a unit test apiece — not of the feature, but of the plumbing: given this session id and this percentage, does bucketing return the same answer every time; does an unknown flag name fail closed; does the env override beat the default. Twenty lines of infrastructure deserve twenty lines of test, and they are the cheapest insurance in the repo, because a bug in the flag layer is a bug in every launch that ever crosses it. The suite runs in milliseconds and has caught exactly one bug ever, which was one production incident, prepaid.

When a platform earns its fee

The wall exists, and here is its shape. The first brick is people: the day someone who does not open the editor needs to change a flag — a support person disabling a feature for one complaining customer, a marketer scheduling a launch for 9 a.m. their time — the repo-and-redeploy model stops being charming and starts being a bottleneck with your name on it. The second brick is audit: the moment a compliance regime or an enterprise customer asks "who changed what, when," a git log of env-var edits is an answer that produces follow-up questions.

The third brick is statistics. Hash bucketing runs an experiment; it does not analyze one. When you genuinely need significance testing, guardrail metrics, and mutually exclusive experiment groups — when decisions hang on measured deltas rather than on "errors flat, revenue fine" — the vendor’s statistics engine is real engineering you should not rebuild from blog posts. The same goes for real-time targeting: percentage ramps need no round trip, but "on for these fifty account ids in this region right now" wants a service, and pretending otherwise means reinventing one badly.

Price the decision like an operator: the platform fee is the visible cost, and the invisible one is that flags-as-a-service tend to become flags-nobody-deletes, because removal now spans a dashboard and a codebase that can drift apart. If you do graduate, carry the disciplines with you — expiries, the ceiling, the monthly audit read against their dashboard instead of flags.ts. The ladder’s lesson does not expire at the top rung: the mechanism was never the point, the practice was, and the practice fits in twenty lines or in an enterprise plan without changing shape.

Frequently asked questions

Quick answers to common questions about this topic.

Do I need a feature flag platform for a small product?

Almost certainly not at first. The practice that matters — separating deploy from release, ramping gradually, keeping kill switches on risky dependencies — is achievable with environment variables, one typed flag module in the repo, and a small hash function for percentage rollouts. A platform starts earning its fee when non-engineers need to change flags, when compliance demands an audit trail of who toggled what, or when you need real experiment statistics rather than a gradual rollout. Until one of those walls appears, the subscription buys convenience you can build in an afternoon.

How do percentage rollouts work without a backend?

With stable bucketing. Hash a stable identifier — a user id, or an anonymous session id for tools without accounts — combined with the flag’s name, take the result modulo 100, and enable the flag when the number falls below your rollout percentage. The computation is deterministic, so each visitor gets a consistent experience with no network call and no stored state, and including the flag name means different experiments select different slices of your audience. Any decent string hash works; FNV-1a is about five lines. Log the bucket decision with your analytics so the two populations can be compared.

How many feature flags is too many?

Fewer than most teams run. The cost is combinatorial rather than linear: every live flag doubles the space of possible system states, so five flags already implies thirty-two combinations nobody has fully tested. I hold a hard ceiling of five simultaneous non-permanent flags, and shipping a sixth means finishing and deleting one of the five first. Permanent kill switches sit outside the ceiling but inside the monthly audit. The signal a flag has overstayed: it can no longer name the decision it is waiting on.

What is a kill switch and what deserves one?

A flag that defaults to on and exists so something can be amputated instantly when it misbehaves — turning diagnose-then-fix into mitigate-then-diagnose. Everything you do not control deserves one: ad scripts, analytics, external fonts, any third-party embed or API. The requirement they impose is graceful absence — the page must be designed to render acceptably with the flagged thing gone — and the maintenance they demand is rehearsal, flipping each switch off on previews quarterly, because a kill switch you have never flipped is a fire extinguisher you have never inspected.

How do I stop feature flags from becoming technical debt?

Treat every flag as born owing you a deletion. Write an expiry date or condition into the flag’s definition at creation; the week a rollout holds at one hundred percent, delete the flag and the losing branch while the context is warm — a typed flag module makes this mechanical, because removing the definition surfaces every call site as a compiler error. Audit the module monthly and require each surviving flag to name the decision it still awaits; anything that cannot answer becomes a documented setting or gets removed. A stale flag is a dead branch kept executable, which is worse than dead code.