Two questions: does it ship, and who has it yet?
"Is this feature on?" is really two questions, and conflating them is how flag systems go wrong. Does this surface exist in this deployment at all? is a release decision, made when you deploy. Which organisations have it yet? is a rollout decision, made by an administrator afterwards, without shipping anything.
So there are two layers. The release gate is a single deployment variable listing the keys this environment ships — blank means none, which is the direction you want a mistake to fail in. The rollout lives in the database: a platform state per flag, plus an override per organisation, both edited from the admin area. One function composes them, in this order:
| Step | What it decides |
|---|---|
| Release gate | the flag asked to be environment-gated and this environment does not list it — off, and nothing below is consulted |
| Override | this organisation was forced on or forced off; that beats the percentage in either direction |
| On / Off | the platform state, applying to everyone with no override |
| Rollout | the organisation (or the person) falls inside the percentage, decided by a hash rather than a coin toss |
| Default | no row exists at all, so the flag's own registry entry answers |
Why the override beats the rollout. That is what an override is for: the design partner who must have it on day one, and the customer who must not get it at all. Being forced is an explicit value on the row and the row's presence is the exception — so the admin screen offers On, Off or Default, never a checkbox that cannot express "I have not decided".
A flag is not a permission
This is the load-bearing rule on the page, and it comes from a real incident rather than a principle. It is tempting to express a flag as something the permission system can answer — the vocabulary is already there, the guards are already written. Do that and a platform administrator, whose permissions are a wildcard over everything, satisfies the check for every flag that exists.
An application built on this kit shipped exactly that. Five routes were open in production to platform staff, while the same feature's dashboards and reporting — gated a different way — stayed correctly dark. Two sources of truth, disagreeing, in a design whose whole point was to have one.
So every gate reads the same list. The resolved flags travel on the authenticated context as a plain array of strings, and the server, the browser and the command line all ask that array. A wildcard permission cannot widen a string comparison. The permission-shaped form still exists for an application that genuinely wants entitlements per customer, but nothing that hides an unreleased surface may use it.
The four doors a feature has to be gated at
Hiding the navigation entry is the part everyone remembers. The kit treats a feature as something with four doors, and the sharp one is not the nav:
| Door | How it is gated |
|---|---|
| API routes | one optional entry beside the route prefix, so the whole surface is dark at once — declared once, like authentication |
| The analytics layer | the cube list is filtered per request, so a gated feature's data is not queryable either |
| Dashboard templates | the one that creates rows — templates seed themselves into every organisation on first load, so an ungated template arrives everywhere the moment you deploy |
| Nav, routes, settings tabs | a guard that takes a flag and a permission as two separate facts, rather than pretending a flag is a permission |
A gated API route answers 404, not 403. A 403 tells the caller the feature exists and they are not allowed it, which is exactly the thing a dark release is trying not to say.
Flag keys are code; the rollout is a hash
A flag key is declared in the shared package beside its description, the same way prompts and agents are. Adding one needs no migration; mistyping one in a gate is a type error rather than a route that quietly 404s for ever. Evaluation walks the registry, so a database row whose key no longer exists is inert by construction — retiring a flag is: remove the gate, deploy, delete the line.
The cost of that choice, stated plainly: an administrator cannot invent a flag key without a deploy. That is deliberate. A flag no code reads does nothing, so the code and its registry line ship together.
Who falls inside a percentage is a hash of the flag key and the unit — an organisation, or a person — rather than a random draw, which buys three properties worth naming:
- Raising a percentage only ever adds. The percentage is not part of what is hashed, so nobody who is already inside a rollout falls out of it when you go from 10% to 25%.
- Flags do not correlate. The key is part of what is hashed, so one flag's first 10% is not the same 10% as the next flag's — otherwise every early rollout lands on the same unlucky customers.
- The same unit always gets the same answer, on every request and in every isolate, with nothing stored.
That hash is a wire format, not an implementation detail: changing it reshuffles every live rollout under way. It is pinned by tests with fixed expected values, so a well-meaning improvement fails loudly instead of silently moving customers in and out of a beta.
Flags resolve with the session, not a second request
Flags are resolved where the session is resolved — one more part of the query that already loads the user, their organisation and their groups — so a request costs nothing extra to know what is on. API-key callers read theirs alongside, with no cache, so a rollout reaches a script on its very next call.
Changing an override for one organisation nudges that organisation, and open browser tabs refresh what they can see without a reload. A platform-wide change does not fan out — that would be one message per organisation on the platform — so it reaches open sessions the next time they ask who they are, which for a rollout is the right trade.
Client-side hiding is cosmetic, as everywhere else in the kit. The gated code still ships in the browser bundle and a determined reader can find it. The server is the protection: the routes are 404, the data is not queryable, and the dashboards do not exist.
What feature flags here are not
There is no per-person targeting beyond choosing to count people rather than organisations in a rollout, no scheduling, and no experiment framework — a flag here answers "who has this yet", not "which variant performed better".
Cloudflare ships a flag product, and it was considered and turned down for this job: its model is your account, your apps, your flags, with no notion of your customers, so per-organisation state would have to live as targeting-rule data that an application writes through an operator API. A toggle is a whole-object write with no safe concurrent update, the audit trail records a token rather than a person, and there is no local store — which would break the promise that this kit runs from a clone with nothing configured. It remains the right tool for rolling the kit's own code across deployments; that is a different job from deciding which of your customers has what.