add-feature-flag
GitHub为Sim系统添加运行时功能标志,支持基于AWS AppConfig的全局或精细化(组织/用户/管理员)控制,并处理非生产环境的密钥回退。
Trigger Scenarios
Install
npx skills add simstudioai/sim --skill add-feature-flag -g -y
SKILL.md
Frontmatter
{
"name": "add-feature-flag",
"description": "Add a runtime feature flag (AppConfig-backed on prod, secret fallback off-prod), global by default or optionally gated by org id, user id, or platform admin",
"argument-hint": "<flag-name>"
}
Add Feature Flag Skill
You add a runtime feature flag to Sim that can change on prod with no redeploy (AWS AppConfig). Prefer a global on/off flag unless the rollout actually needs per-organization, per-user, or platform-admin targeting. When AppConfig isn't the source of truth, the flag falls back to a single secret (on/off only).
When to use this vs env-flags.ts
- Feature flag (
@/lib/core/config/feature-flags.ts): runtime global on/off by default, optionally scoped byuserId/orgId/admin. This skill. - Env flag (
@/lib/core/config/env-flags.ts): deploy-time capability/environment detection (isProd,isHosted,isBillingEnabled). A module-load boolean. Do not add gated flags here.
If the user wants a fixed per-deployment toggle, send them to env-flags.ts instead.
The flag model
A flag's gating rule lives only in the hosted AppConfig document. It is ON for a context when any configured clause matches:
interface FeatureFlagRule {
enabled?: boolean // global default for everyone
orgIds?: string[] // allowlisted organization ids
userIds?: string[] // allowlisted user ids
adminEnabled?: boolean // platform admins (user.role === 'admin')
}
Critically, none of this is expressible in code — gating (especially adminEnabled) can only be set through AppConfig, so no environment can grant access from a code literal. Off-AppConfig (self-hosted/OSS/local), a flag is simply on or off, derived from its fallback secret.
Steps
-
Confirm the granularity before editing code. If the user has not already specified it, stop and ask:
Should
<flag-name>be a global on/off flag (recommended), or does it need rollout targeting by organization, user, and/or platform admin?- Recommend global. Do not infer scoped gating merely because the call site already has a user or organization id.
- If the user chooses scoped gating but does not name the dimensions, ask which of organization, user, and platform admin it needs. Wire only the selected dimensions.
- If the user wants a fixed per-deployment toggle rather than a runtime AppConfig flag, use
env-flags.tsinstead.
-
Define the flag. Add one entry to the
FEATURE_FLAGSregistry inapps/sim/lib/core/config/feature-flags.ts. Each entry is the flag's whole definition — name (kebab-case key),description, and thefallbacksecret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally):const FEATURE_FLAGS = { '<flag-name>': { description: '<what this gates>', fallback: '<FLAG_SECRET>', }, }fallbackis the env/secret key (typed askeyof typeof env), so add<FLAG_SECRET>toapps/sim/lib/core/config/env.tsfirst (and the deployment's secret store) — it won't typecheck otherwise. Do not add org/user/admin defaults here — that gating exists only in AppConfig. Adding the entry makes<flag-name>a validFeatureFlagName. -
Gate the call site at the chosen granularity. For the recommended global mode, pass no context:
import { isFeatureEnabled } from '@/lib/core/config/feature-flags' if (await isFeatureEnabled('<flag-name>')) { // gated behavior }Do not fetch, resolve, or thread through user or organization context solely for a global flag.
For scoped rollout, pass only the dimensions the user selected. Admin status is resolved internally, so ordinary callers pass
userId, not a role:import { isFeatureEnabled } from '@/lib/core/config/feature-flags' if (await isFeatureEnabled('<flag-name>', { userId, orgId })) { // gated behavior }- Organization targeting uses
orgId; user and platform-admin targeting requireuserId. - Missing ids are fine — a clause with no matching id is skipped; with no
userId, the admin clause resolves tofalsewithout a DB read. - Admin routes that already know the caller is an admin may pass
{ userId, isAdmin: true }to skip the role lookup. - Client/UI flags: resolve server-side (in a server component, route, or loader) and pass the boolean down as a prop. There is no client AppConfig.
- Organization targeting uses
-
(Prod) configure in AppConfig. The infra
feature-flagsprofile schema is permissive, so a new flag needs no infra change. Operators add the flag to the hostedfeature-flagsdocument usingenabledfor global rollout or only the selectedorgIds/userIds/adminEnabledclauses for scoped rollout, then start asim-<env>-fastdeployment (see the AppConfig runbook in the infra README — same flow asaccess-control). The fallback secret only applies when AppConfig is disabled. -
Test. Add a case to
apps/sim/lib/core/config/feature-flags.test.tsthat matches the chosen granularity. For a global flag, exerciseisFeatureEnabled('<flag-name>')with an AppConfigenabledrule and toggle the fallback secret for the off-AppConfig path. For scoped rollout, cover only the selected clauses and mockisPlatformAdminwhen testingadminEnabled. -
Clean up after rollout. When the feature ships to everyone, delete the flag's entry from
FEATURE_FLAGS, the<FLAG_SECRET>env entry, the AppConfig document, the call sites, and the test. Leaving dead flags around is the main failure mode of flag systems.
Notes
- Flag keys are
kebab-case. - Never read flags via raw
fetchor a new AppConfig client — always go throughisFeatureEnabled/getFeatureFlags. - Never bake gating into code. The fallback is a single boolean secret; org/user/admin scoping is AppConfig-only.
- Never add or propagate request context unless the user chose scoped rollout.
- The admin check reads the DB replica (
dbReplica) and is resolved lazily, so an admin-gated flag adds at most one cheap replica read, and only whenadminEnabledis the deciding clause.
Version History
- ceda457 Current 2026-08-20 15:28


