TL;DR: A small team does not need a feature flag platform, an experimentation dashboard or a hundred toggles. It needs a handful of kill switches on the things that are expensive to undo: features inside an app that has to pass store review, integrations that write to a client's CRM or calendar, and any change to a live phone agent. Keep flags in one server-side place, give every flag a safe default for when that place is unreachable, scope flags by app version and platform, decide in writing who is allowed to flip each one, and delete release flags within a few weeks of a feature going fully live. A/B testing is a different job, and most early products do not have the traffic for it yet. I have run this setup as CTO at RAQTS across Unity, React Native and Next.js, and the same rule shapes how I ship voice agents for clients.
The question I ask about any change before it goes out is not "will this work?" It is "if this is wrong, how long until it stops hurting?" For a website on a modern host, the answer is minutes: redeploy the previous version. For a mobile app, it is the length of a store review plus however long people take to update, which for some users is never. For a voice agent answering a clinic's phones, it is every call that comes in while you are still figuring out what went wrong.
Feature flags are how you make that answer short everywhere, including the places where a rollback is slow. On a small team, that is the whole point of them.
What a feature flag actually is
A feature flag is a value the running software reads at runtime to decide which path to take, stored somewhere you can change without shipping new code. It can be a database row, a remote config key, or a field in a JSON document your backend serves. The mechanism matters less than the decision you have moved out of the build and into something you control live.
Flags get used for several different jobs, and mixing them up is how teams end up with a mess:
- Kill switches. Turn a feature or integration off, fast, when it misbehaves. Long-lived, rarely touched, and the most valuable kind.
- Release flags. Ship code dark, then turn it on when you are ready. Short-lived. Should be deleted once the feature is fully live.
- Operational settings. Limits, thresholds, which provider to call. Closer to configuration than to flags, but they live in the same place.
- Experiments. Show variant A to some users and B to others, then measure. A statistics problem as much as an engineering one.
On a team of two to nine people, I build the first two from day one, treat the third as normal configuration, and push back on the fourth until the product has enough traffic to make the numbers mean something.
Where kill switches earn their keep
Not every feature needs a flag; each one adds a branch to test and clean up later. I put them where undoing a mistake is slow or the damage spreads to someone else's system.
Anything inside a store-reviewed app
At RAQTS the product spans a Unity app called Lumo, React Native mobile apps and a Next.js web portal, all reading from one backend, with releases going through App Store and Play Console review. The web portal can be fixed in minutes. A shipped mobile binary cannot. If a new screen crashes on a particular device, or a new flow confuses people, the fix is at least a review cycle away, and older versions linger on phones long after that.
So new features in the store builds sit behind a server-controlled flag that can be scoped by app version and platform. That turns "wait for review and hope" into "turn it off now, fix it calmly, ship the fix". It also means a feature can land in a build before it is switched on, so the review calendar stops dictating launch dates. I covered the wider version of this problem in supporting old mobile app versions; flags are one of the rules there, and the one I would refuse to skip.
Integrations that write to someone else's system
A bug that only affects your own screen is embarrassing. A bug that writes junk into a client's CRM, double books their calendar or texts their customers twice is a cleanup project with an apology attached. Every integration that writes outward gets its own off switch, separate from the feature that triggers it.
In the voice agent work I have done at Fortell AI and Tested Media, that means the booking step, the CRM update and the follow-up SMS can each be disabled independently of the agent itself. If the calendar integration starts failing, the agent should not go silent or start apologising on every call. With the booking switch off it falls back to taking the caller's details and promising a callback, which is a worse experience but a safe one. The same thinking runs through how I plan for voice agent outages and failover: decide in advance what degraded looks like, then make it one switch away.
Live phone agents
Changing a live agent's prompt or flow is a deploy, even when the platform makes it feel like editing a document. I keep the previous version of an agent available and route between versions, so rolling back is a routing change rather than a rewrite under pressure. Paired with regression testing for voice agents, that makes changes boring, which is exactly what a business owner wants from the thing answering their phone.
The rules I follow when building flags
One place, read on the server where possible
Flags scattered across environment variables, constants and two different services are flags nobody can find at 11pm. I keep them in one place the backend serves, and where I can, the server makes the decision rather than every client. If the server decides whether a feature is available, fixing a bad decision fixes it for every app version at once. It is the same reason RAQTS's backend owns leaderboard ordering instead of each client computing it.
Every flag has a safe default
The flag service will be unreachable at some point: no network on the device, a cold start before config has loaded, a bad deploy of the config itself. The code has to know what to do with no answer. My default is that new and risky features fail closed, so they stay off, and safety features fail open, so a fallback stays on. Write the default next to the flag definition, not in someone's head. A kill switch that defaults to "feature on" when config cannot load is not a kill switch.
Scope by version and platform
On mobile, a flag that is simply on or off for everyone is too blunt. The bug is often in one platform, or in one range of builds, and turning the feature off everywhere punishes the users who were fine. I want to be able to say "off for iOS builds below this version" without a code change. That needs the app to send its version and platform with every request, which is worth doing from version one anyway.
Decide who is allowed to flip it
A switch nobody is authorised to touch is decoration. For each kill switch I write down who can flip it and under what conditions. For client voice agents, that list includes someone at the business, with a short runbook and screenshots, because I will not always be the first person awake when something goes wrong. That handover is part of how I hand a voice agent over to a client.
Log every change
Every flag change gets recorded: what, who, when, and why. When a metric moves, the first question is "what changed?", and a flag flip is a change just as much as a deploy.
Flag debt is real debt
Release flags are supposed to be temporary. In practice they become permanent branches nobody dares delete, and each one doubles the paths through that code.
What keeps it under control:
- Every release flag gets an owner and a removal date when it is created, not later.
- Removal is part of the feature. The feature is done when it is fully on and the flag and the dead branch are gone, not when it first ships.
- Kill switches are labelled as permanent so nobody deletes them in a cleanup pass, and they get exercised now and then so you know they still work.
- Both sides of a flag get tested while it exists. A path you never run is a path that is quietly broken.
This is the same lens I use for technical debt in general: a stale flag on code you change every week charges interest every week. In code review, "how do we turn this off, and when do we delete the flag?" is a question I ask on any change that ships to a store or a phone line.
When you actually need experiments
A/B testing gets the most attention and is what small products need least. An experiment only tells you something if each variant gets enough users to separate a real difference from noise. Early on, you rarely have that. You learn more from watching ten people use the product, or listening to twenty call recordings, than from splitting a few hundred sessions two ways. Once a product has real volume, the flags you already have become the delivery mechanism, and a dedicated platform may earn its cost.
The minimum setup I would build tomorrow
If I were starting a new product this week, this is all I would add on day one:
- A flags table or config document the backend serves, with a name, value, safe default, owner, removal date and description for each flag.
- The app sending its version and platform on every request, so flags can be scoped.
- A kill switch around each outward integration and each new feature in a store build.
- A change log, even if it starts as an append-only table.
- A one-page note saying who can flip which switch, and what the product does when each one is off.
That is a day or two of work, and it changes the answer to "how long until this stops hurting?" from days to seconds.
I work as a hands-on fractional CTO and engineer for startups in the US, UK and Europe, across web, mobile, Unity and XR, and production AI voice agents. If you are about to ship something you cannot easily take back and want a second pair of eyes on how to make it safe, read more about how I work as a fractional CTO, or book a call.