Shipping under a flag is not finishing the flag
Feature flags are how platforms ship dark and recover without a redeploy. They are also how codebases grow a second set of branches nobody owns. On BuilderHelp we treat a release flag as temporary infrastructure: it needs a named owner, a type, and a removal date at creation. Turning the flag on for everyone is a milestone. Deleting the dead path is the finish line. Leaving the gate in place because 'it still works as a kill switch' is how you end up maintaining two products inside one deploy.
Birth is cheap. Death is the discipline.
Creating a flag is one PR and a dashboard toggle. Cleaning it up is a second PR that has to find every branch, every client that still evaluates it, and every runbook that still mentions it. Most flag debt is unfinished death. The inventory grows because removal was never scheduled when the flag was born.
We refuse the pattern of 'add now, tidy later.' Later arrives as a quarterly cleanup that nobody wants to staff. The cheaper habit is metadata at creation: who owns it, what kind of flag it is, and when it should be gone. A flag without those three fields is not temporary. It is a permanent fork with a friendly name.
This is different from where the gate lives. We already put capability checks in the API when a change crosses the web app, the Expo field client, and the OCR worker. Ownership answers a different question: once the rollout is over, who is responsible for collapsing the temporary branch back into one path.
Name the type before you name the flag
Release flags and operational flags look identical in a boolean SDK. They are not the same object. A release flag exists to hide unfinished code until a cohort is ready, then it should disappear. An operational flag — a kill switch for a flaky printer path, a maintenance window, a vendor fallback — may stay for years because flipping it is the remediation.
Mixing those types is how cleanup stalls. Someone refuses to delete a release flag because 'we might need the kill switch,' while the operational flag never gets an owner because it was created as a temporary experiment. Tag the type in the name or metadata at birth. Cleanup rules follow the type: release and experiment flags age out on a short clock; operational flags get an owner and a review cadence instead of a deletion fantasy.
On a multi-surface product the type also tells you which clocks must move before removal is safe. A release flag that gated a new receipt field may still be referenced in last month's Expo binary. The web and OCR worker can drop the branch after their deploys; the mobile path waits until the store-reviewed build is no longer in the wild. The removal date is a plan, not a calendar surprise.
Ownerless flags become immortal
A shared 'platform' owner is the same failure mode as a shared observability dashboard. When every flag belongs to the team, none of them belong to a person who will open the removal PR. We assign an owner at creation — usually the engineer who merges the feature — and we treat a past-due removal date as a bug on their plate, not a nice-to-have chore.
The owner is accountable for the whole lifecycle, not just the happy path. That includes filing the removal ticket before the flag goes live, watching whether the default can stay on without the branch, and coordinating the client that still ships the old evaluation. If the owner leaves the project, the flag is reassigned the same week. Orphaned flags are how inventories go silent.
We keep the inventory readable: type, owner, created date, target removal, and which surfaces still evaluate it. A dashboard that only shows on/off percentages without ownership is a status wall, not a lifecycle tool.
Definition of done includes the deleted branch
A feature is not done when production traffic hits the new path. It is done when the old path is gone from the code that still ships, the flag is archived in the platform, and the runbook no longer tells someone to flip a toggle that no longer exists. Until then you are carrying two implementations, two test matrices, and a mental model that says 'check the flag' long after the product decision was settled.
The practical sequence matches how we already ship across clocks. Compatible server first. Clients that understand the new shape next. Widen the cohort. Keep the flag as a kill switch only while the feature is still earning trust. Then remove: one PR that deletes the dead branch, one deploy per surface that still evaluated it, then delete or archive the flag in the platform after the code no longer references it. Deleting the config while clients still call it is how you invent a surprise default.
If a flag keeps getting extended past its date, that is a signal the feature is not actually done — or that it was never a release flag and should be reclassified as operational with an explicit owner. Extending silently is how temporary becomes forever.
What we put in the same PR
When we add a release flag on BuilderHelp, the PR that introduces the branch also records the owner, the type, the target removal date, and the abort story — what metric or incident flips it off without a redeploy. The removal ticket is opened then, not after launch week. That sounds ceremonial until you have spent a day untangling three nested conditions that all defaulted to 'on' and nobody remembered why.
Quarterly cleanup still happens, but it should be a backstop for misses, not the primary process. The report lists flags past their date, flags with no owner, and flags that have been 100% on (or off) long enough that the branch is fiction. Each item gets one removal PR. Platform deletion waits until the references are gone from every surface still in the wild.
The point is not a larger flag platform. It is a smaller inventory of temporary forks, each with a person who will finish the work the toggle started.