4.5 KiB
description, argument-hint
| description | argument-hint |
|---|---|
| Add a runtime gated feature flag (AppConfig-backed on prod, secret fallback off-prod), gated by org id, user id, or admin | <flag-name> |
Add Feature Flag Skill
You add a runtime, gated feature flag to Sim — one that can be turned on for specific orgs, users, or admins and changed on prod with no redeploy (AWS AppConfig). 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): per-request, gated byuserId/orgId/admin, changeable at runtime. 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 clause matches:
interface FeatureFlagRule {
enabled?: boolean // global default for everyone
orgIds?: string[] // allowlisted organization ids
userIds?: string[] // allowlisted user ids
admins?: boolean // platform admins (user.role === 'admin')
}
Critically, none of this is expressible in code — gating (especially admins) 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
-
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. Call
isFeatureEnabledwith whatever ids you have — admin status is resolved internally, so callers never pass it:import { isFeatureEnabled } from '@/lib/core/config/feature-flags' if (await isFeatureEnabled('<flag-name>', { userId, orgId })) { // gated behavior }- 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.
- Missing ids are fine — a clause with no matching id is skipped; with no
-
(Prod) configure in AppConfig. The infra
feature-flagsprofile schema is permissive, so a new flag needs no infra change. Operators add the flag underflagsin the hostedfeature-flagsdocument — including anyorgIds/userIds/adminsgating — and 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.ts: usewithAppConfig({ flags: { ... } })to cover the gating rule (mockisPlatformAdminfor theadminsclause), and toggle the fallback secret to cover the off-AppConfig path. -
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.
- 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 whenadminsis the deciding clause.