Skip to main content
The @ooneex/feature-flag component is a lightweight, type-safe contract for gating functionality behind toggles. Each flag is a small, injectable class implementing the IFeatureFlag interface — getKey, getDescription, and isEnabled — registered with a decorator and resolved from the container. The enablement check can be synchronous or asynchronous, so a flag can return a constant, read an environment variable, or query a remote service.

Why this component

  • One focused contract. Every flag implements the same IFeatureFlag interface, so call sites evaluate them the same way regardless of how each flag decides.
  • Sync or async. isEnabled() may return a boolean or a Promise<boolean> — back a flag with a constant, an env var, config, or a remote check without changing callers.
  • Self-describing. Each flag carries a stable getKey() and a human-readable getDescription(), so flags are discoverable and auditable.
  • Container-managed. Register a flag class with a decorator and resolve it from the DI container with any scope.
  • Type-safe. Full TypeScript support with IFeatureFlag and FeatureFlagClassType.

How it works

You implement IFeatureFlag as a class, register it with the featureFlag decorator, and resolve it from the container. Callers invoke isEnabled() and branch on the result.
MethodPurpose
getKey()Return a unique, stable identifier for the flag (kebab-case by convention).
getDescription()Return a human-readable explanation of what the flag controls.
isEnabled()Decide whether the flag is on; returns boolean or Promise<boolean>.
The interface is the whole contract:
interface IFeatureFlag {
  getKey: () => string;
  getDescription: () => string;
  isEnabled: () => Promise<boolean> | boolean;
}
Because isEnabled() can be async, the same flag shape covers constant toggles, environment-driven rollouts, and remote configuration — callers always await the result and never need to know the source.

Decorator and usage

@decorator.featureFlag(scope?)

Registers a feature-flag class with the DI container. It accepts an optional EContainerScope (defaults to EContainerScope.Singleton). Implement IFeatureFlag and register it:
import { decorator, type IFeatureFlag } from "@ooneex/feature-flag";

@decorator.featureFlag()
export class DarkModeFeatureFlag implements IFeatureFlag {
  public getKey(): string {
    return "dark-mode";
  }

  public getDescription(): string {
    return "Enables the dark mode theme";
  }

  public isEnabled(): boolean {
    return true;
  }
}
isEnabled() can resolve a value from a remote service, database, or environment:
import { decorator, type IFeatureFlag } from "@ooneex/feature-flag";

@decorator.featureFlag()
export class BetaCheckoutFeatureFlag implements IFeatureFlag {
  public getKey(): string {
    return "beta-checkout";
  }

  public getDescription(): string {
    return "Enables the redesigned checkout flow for beta users";
  }

  public async isEnabled(): Promise<boolean> {
    return process.env.BETA_CHECKOUT === "true";
  }
}
Resolve the flag from the container and branch on the result:
import { container } from "@ooneex/container";
import { DarkModeFeatureFlag } from "./DarkModeFeatureFlag";

const flag = container.get(DarkModeFeatureFlag);

if (await flag.isEnabled()) {
  // ...render dark mode
}
Pass a scope when a flag should not be a singleton — for example, re-evaluate per request:
import { EContainerScope } from "@ooneex/container";
import { decorator, type IFeatureFlag } from "@ooneex/feature-flag";

@decorator.featureFlag(EContainerScope.Transient)
export class ExperimentalFeatureFlag implements IFeatureFlag {
  public getKey(): string {
    return "experimental";
  }

  public getDescription(): string {
    return "Enables experimental features";
  }

  public isEnabled(): boolean {
    return false;
  }
}

Best practices

  • Keep keys stable and unique. Use kebab-case keys like dark-mode and beta-checkout; never reuse or rename a key once it ships, since logs and config may reference it.
  • Write a real description. getDescription() is what makes flags auditable — explain what the flag controls and who it targets, not just its name.
  • Always await the result. Treat isEnabled() as potentially async even for constant flags, so a flag can later move to a remote check without touching callers.
  • Keep the decision in one place. Put all gating logic inside isEnabled(); callers should only branch on the boolean, never re-derive the condition.
  • Match the scope to the source. Use the default singleton for constant or env-driven flags; use Transient or Request when the flag must be re-evaluated per request.
  • Inject dependencies. When a flag reads config or calls a service, inject those dependencies through the constructor rather than reaching for globals.

CLI command

Scaffold a feature flag class and its test file with the generator. It writes the class under modules/<module>/src/flags/<Name>FeatureFlag.ts and installs @ooneex/feature-flag if it is missing.
# Interactive: prompts for the name
ooneex flag:create

# Provide the name
ooneex flag:create --name=NewCheckout

# Target a module and overwrite
ooneex flag:create --name=NewCheckout --module=checkout --override
OptionDescriptionDefault
--nameFeature flag class name. The FeatureFlag suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class is a ready-to-complete stub with the key pre-filled in kebab-case from the name:
import { decorator, type IFeatureFlag } from "@ooneex/feature-flag";

@decorator.featureFlag()
export class NewCheckoutFeatureFlag implements IFeatureFlag {
  public getKey(): string {
    return "new-checkout";
  }

  public getDescription(): string {
    return "";
  }

  public isEnabled(): Promise<boolean> | boolean {
    return false;
  }
}
See flag:create for the full command reference.

Use with Claude and Codex

The generator ships a matching flag:create skill. It runs the scaffold and then guides your AI agent through completing the flag — setting a stable key, writing the description, and implementing isEnabled() with the real gating logic. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a feature flag that enables the new checkout flow.
For example, the prompt above maps to flag:create --name=NewCheckout, then implements getKey, getDescription, and isEnabled for the new checkout flow.