Feature Flags for Vibecoded Apps
Ship half-finished AI-generated features safely by gating them behind flags instead of leaving them half-merged on a branch.
Feature Flags for Vibecoded Apps
A common vibecoding failure mode: you ask an assistant to build a feature, it mostly works, and now you're stuck choosing between merging something half-finished or leaving it stranded on a branch that drifts further from main every day. Feature flags solve this directly — merge the code as soon as it's safe to ship dormant, then turn it on when it's actually ready. The code lives in main from day one; only its visibility changes.
The simplest possible flag
You don't need a vendor or a dashboard to start. A flag is just a boolean that gates a render or a code path:
// lib/flags.ts
const FLAGS = {
newCheckoutFlow: process.env.NEXT_PUBLIC_FLAG_NEW_CHECKOUT === "true",
aiSummaries: process.env.NEXT_PUBLIC_FLAG_AI_SUMMARIES === "true",
} as const;
export function isEnabled(flag: keyof typeof FLAGS): boolean {
return FLAGS[flag];
}
import { isEnabled } from "@/lib/flags";
function Checkout() {
if (isEnabled("newCheckoutFlow")) {
return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;
}
This alone is enough to unblock the "merge or don't" dilemma. The unfinished feature ships in the same PR as everything else, gated off by default, and turning it on in production is an environment-variable change — no redeploy of code, no rushed merge the night before a demo.
Per-user flags when you need gradual rollout
An env-var flag is all-or-nothing for every user. Once you want to dogfood a feature yourself before your whole user base sees it, you need the flag decision to depend on who's asking:
// lib/flags.ts
interface FlagContext {
userId?: string;
isInternalUser?: boolean;
}
const ROLLOUT_PERCENT = {
aiSummaries: 20, // 20% of external users
} as const;
function hashToPercent(input: string): number {
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
}
return hash % 100;
}
export function isEnabled(flag: keyof typeof ROLLOUT_PERCENT, ctx: FlagContext): boolean {
if (ctx.isInternalUser) return true; // internal users always get new features first
if (!ctx.userId) return false;
return hashToPercent(ctx.userId + flag) < ROLLOUT_PERCENT[flag];
}
Hashing the user id deterministically (rather than rolling random per request) matters: the same user gets the same answer on every page load, so they don't flicker between the old and new experience on refresh. Bump ROLLOUT_PERCENT from 20 to 50 to 100 as confidence grows, with real usage data at each step instead of a single all-at-once cutover.
Flags are also a safety valve, not just a rollout tool
The same mechanism doubles as an incident response tool. If an AI-generated feature ships and starts misbehaving in production, flipping its flag off is instant and doesn't require a revert-and-redeploy cycle under pressure. This is the strongest argument for wrapping any meaningfully new AI-generated feature in a flag before it reaches real users, even if you're confident in it — the flag costs almost nothing to add and buys you an emergency off switch you'll be glad to have exactly once.
The part people skip: cleaning up
Flags left in place after a feature has fully shipped are their own form of debt — dead branches, an isInternalUser check nobody remembers the reason for, a LegacyCheckoutFlow component still imported "just in case." Once a flag has been at 100% for a reasonable stretch with no incidents:
- [ ] Delete the
if (isEnabled(...))branch and inline the new path permanently - [ ] Delete the old code path it replaced — don't leave it as dead code "for reference"
- [ ] Remove the flag's entry from
FLAGS/ROLLOUT_PERCENT - [ ] Remove the now-unused env var from your deployment config
A flag that's been fully rolled out for months and never cleaned up is worse than no flag at all — it's an extra branch every future reader has to reason about, gating a decision that was already made.
The takeaway
Flags turn "is this feature done enough to merge" into two separate, much easier questions: "is this code safe to merge dormant" (usually yes, quickly) and "is this feature ready for users" (answered later, with real data, at your own pace). The cost is a few lines of gating logic and one disciplined cleanup step once the rollout finishes — cheap, compared to the alternative of a stale branch or a rushed all-at-once launch.
Stay in the flow
Get vibecoding tips, new tool announcements, and guides delivered to your inbox.
No spam, unsubscribe anytime.