--- name: 'feature-flags-authoring' description: 'Add, edit, or remove a Packmind feature flag across the shared registry, the frontend gate, and the backend helper. Use when the user wants to create a new feature flag, gate a feature behind a flag, open or restrict a flag to a domain/email, change a flag audience, retire or delete a flag, or asks how the feature-flag system is wired. This is the developer how-to counterpart to the feature-flags-audit skill.' --- # Feature Flags Authoring A developer how-to for the full lifecycle of a Packmind feature flag: **add**, **edit**, **remove**. Flags gate a feature by **email domain** (or exact email). The same decision function runs on frontend and backend, so behavior never diverges. Follow the steps below exactly — every flag touches three places. For inventorying existing flags, orphan detection, and audience audits, use the **`feature-flags-audit`** skill instead. This skill changes flags; that one scans them. ## Where things live - **Shared registry — `@packmind/feature-flags`** (pure, `env:shared`, browser-safe). The single source of truth: - `registry.ts` — the `*_FEATURE_KEY` constants, `DEFAULT_FEATURE_DOMAIN_MAP` (`Record` mapping each flag's string value to allowed entries), and the `FeatureFlagKey` union. - `isFeatureFlagEnabled.ts` — the pure evaluator: `isFeatureFlagEnabled({ featureKeys, featureDomainMap, userEmail })`. - **Frontend gate** — `` from `@packmind/ui` (or a direct `isFeatureFlagEnabled(...)` call). The keys, `DEFAULT_FEATURE_DOMAIN_MAP`, and `isFeatureFlagEnabled` are re-exported from `@packmind/ui` for back-compat, so existing consumers can import from either place. - **Backend gate** — `isFeatureEnabled(flag, { userEmail })`, a plain async helper from `@packmind/node-utils`. It resolves the **env kill-switch** first, then defers to the shared email-domain rule. It is NOT a port and has no DI — import it and call it. - **Env kill-switch** — `FF_` (e.g. `FF_CHANGE_PROPOSALS_IN_WEBAPP`). `on`/`all`/`true` force the flag on; `off`/`none`/`false` force it off; unset or empty falls through to the email-domain map. Two flags exist today: `'change-proposals-in-webapp'` and `'orga-space-management'`. ## Add a flag ### 1. Register it in `@packmind/feature-flags` Add the key constant, the audience entry, and extend the union. Do all three — a constant with no map entry is never enabled for anyone; a map entry with no union member can't be typed at call sites. ```ts // registry.ts export const MY_FEATURE_KEY = 'my-feature' as const; export type FeatureFlagKey = | 'change-proposals-in-webapp' | 'orga-space-management' | 'my-feature'; export const DEFAULT_FEATURE_DOMAIN_MAP: Record = { // ... 'my-feature': ['@packmind.com'], // domains keep the `@`; exact emails have no leading `@` }; ``` ### 2. Gate the frontend Wrap the UI that should appear only when the flag is on: ```tsx import { PMFeatureFlag } from '@packmind/ui'; import { MY_FEATURE_KEY, DEFAULT_FEATURE_DOMAIN_MAP } from '@packmind/feature-flags'; {/* gated UI */} ``` For a branch (route, tab, boolean), call `isFeatureFlagEnabled({ featureKeys: [MY_FEATURE_KEY], featureDomainMap: DEFAULT_FEATURE_DOMAIN_MAP, userEmail })` directly. ### 3. Gate the backend Inside a member use case, `command.user.email` is available. Branch on the helper: ```ts import { isFeatureEnabled } from '@packmind/node-utils'; import { MY_FEATURE_KEY } from '@packmind/feature-flags'; if (await isFeatureEnabled(MY_FEATURE_KEY, { userEmail: command.user.email })) { // new path } else { // old path } ``` ### 4. (Optional) Env override Set `FF_MY_FEATURE=on` to force the flag on everywhere, or `FF_MY_FEATURE=off` to force it off, regardless of the domain map. Leave unset to defer to the map. Document the var in the deploy config if you rely on it. ## Edit a flag Change the allowed audience in `DEFAULT_FEATURE_DOMAIN_MAP` — add or remove domains (`@company.com`) or exact emails (`user@company.com`). This is the only place audience lives; the frontend and backend both read it. Remember: the `FF_*` env kill-switch **overrides the map at runtime**. If a flag looks stuck on or off in an environment, check the env var before editing the map. ## Remove a flag 1. In `@packmind/feature-flags`: delete the `*_FEATURE_KEY` constant, its `DEFAULT_FEATURE_DOMAIN_MAP` entry, and its `FeatureFlagKey` union member. 2. Delete every call site: - Frontend: remove the `` wrappers (keep the children — the UI ships unconditionally now) and any `isFeatureFlagEnabled` branches. - Backend: remove the `isFeatureEnabled` branches, keeping the winning branch inline. 3. Drop any `FF_` env var from deploy config. 4. Run the **`feature-flags-audit`** skill to confirm no orphaned keys, map entries, or call sites remain. ## Conventions - **Audience is email-domain only** (Proposal A). No org, plan, team, or percentage targeting exists yet — do not describe or build it. - **Never log an email in clear text.** If you log a flag decision, mask the email (first 6 chars + `*`) or, better, log only the flag key and the boolean outcome. - **Keep `@packmind/feature-flags` pure and browser-safe.** No `env:node` imports (no `@packmind/node-utils`, no `Configuration.getConfig`, no `process.env`). It ships to the browser bundle. - **The backend gate is a plain helper call, not an injected port.** Import `isFeatureEnabled` and call it directly; do not add a port/adapter/registry. ## See also - **`feature-flags-audit`** — the review/scan counterpart: inventories every flag, its audience, its usages, and flags orphans. Run it after removing a flag and whenever you need the current state of the system.