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.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add PackmindHub/packmind --skill feature-flags-authoring --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Feature Flags Authoring?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/packmindhub-feature-flags-authoring-96177be8)More formats (shields.io, HTML) on the badges page.
---
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<string, readonly string[]>` mapping each flag's string value to allowed entries), and the `FeatureFlagKey` union.
- `isFeatureFlagEnabled.ts` — the pure evaluator: `isFeatureFlagEnabled({ featureKeys, featureDomainMap, userEmail })`.
- **Frontend gate** — `<PMFeatureFlag>` 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_<SCREAMING_SNAKE>` (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<string, readonly string[]> = {
// ...
'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';
<PMFeatureFlag
featureKeys={[MY_FEATURE_KEY]}
featureDomainMap={DEFAULT_FEATURE_DOMAIN_MAP}
userEmail={currentUser.email}
>
{/* gated UI */}
</PMFeatureFlag>
```
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 `<PMFeatureFlag>` 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_<SCREAMING_SNAKE>` 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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!