Most feature flag implementations look like this:
if (process.env.ENABLE_BILLING === "true") {
// billing logic
}
Sprinkled across route handlers, server actions, React components, and nav menus. No type safety. No enforcement. Just strings and hope.
It works until it doesn't. And when it doesn't, you get phantom routes, dead code, inconsistent access, and users seeing UI for features that don't exist on the server.
The problems with env booleans
Drift. You add a flag in one file. Someone else checks a different env var in another file. Now you have two sources of truth for the same feature.
No compile-time safety. Typo in the env name? It evaluates to undefined, which is falsy, which silently disables the feature. No error. No warning.
Dead code accumulation. You ship a feature behind a flag. Six months later, the flag is always on. But the conditional is still in 14 files. Nobody removes it because nobody knows if it's safe to.
Inconsistent gating. The route handler checks the flag. The nav link doesn't. The server action does. The API endpoint doesn't. Now users can reach a page that calls an action that throws.
What feature gating actually requires
A feature flag system needs three things:
- A single source of truth. One config object. Typed. Importable.
- Enforcement at every layer. Not just the UI. Not just the route. Every layer.
- Compile-time guarantees. If you reference a flag that doesn't exist, it should fail the build.
How SaaSCoreX handles it
SaaSCoreX defines features as a typed config object:
export const features = {
billing: "stripe" as "off" | "stripe",
teams: true,
onboarding: true,
auditLog: true,
admin: true,
waitlistMode: false,
impersonation: false,
apiKeys: true,
} as const;
export type FeatureFlag = keyof typeof features;
export function isFeatureEnabled(flag: FeatureFlag): boolean {
const value = features[flag];
if (typeof value === "boolean") return value;
if (typeof value === "string") return value !== "off";
return false;
}
FeatureFlag is a union type derived from the config. Reference a flag that doesn't exist and TypeScript catches it at compile time. Not at runtime. Not in production.
Three layers of enforcement
This is where most implementations stop. SaaSCoreX enforces at every boundary:
1. Server actions — throw if disabled:
export function assertFeatureEnabled(flag: FeatureFlag): void {
if (!isFeatureEnabled(flag)) {
throw new FeatureDisabledError(flag);
}
}
// Usage in a server action:
export async function createInvite(data: InviteInput) {
"use server";
assertFeatureEnabled("teams");
// ... rest of the action
}
2. Route handlers — return 404 if disabled:
export function withFeatureGuard(flag: FeatureFlag, handler) {
return async (...args) => {
if (!isFeatureEnabled(flag)) {
return new NextResponse(null, { status: 404 });
}
return handler(...args);
};
}
// Usage:
export const POST = withFeatureGuard("billing", async (request) => {
// ... handler logic
});
3. React components — hide UI if disabled:
export function FeatureGate({ flag, children, fallback = null }) {
if (!isFeatureEnabled(flag)) return <>{fallback}</>;
return <>{children}</>;
}
// Usage:
<FeatureGate flag="teams">
<SidebarLink href="/app/team">Team</SidebarLink>
</FeatureGate>
When a feature is off, routes don't exist, nav entries don't render, and server actions throw before any side effect executes. There's no inconsistency because there's no way to partially enable a feature.
Why this matters for real products
You might not need all of SaaSCoreX's subsystems on day one. Maybe you launch solo — no teams, no billing, no audit log.
With env booleans, that means commenting out code, leaving dead imports, and hoping nobody calls the wrong endpoint.
With typed feature gating, you set teams: false in one file. Every route, action, and UI element that depends on teams vanishes. Cleanly. No dead code. No phantom routes. No "we usually check the flag first."
When you're ready to enable teams, you change one value. Everything lights up. Because the wiring was always there — it was just gated.
The principle
Feature flags are not configuration. They are architecture.
If your flag system relies on string matching and scattered conditionals, it will drift. If it relies on types and structural enforcement, it won't.
This is one of nine server-enforced subsystems in SaaSCoreX. See the full implementation on the architecture page.