You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/commands/add-feature-flag.md
+14-13Lines changed: 14 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,11 +1,11 @@
1
1
---
2
-
description: Add a runtime gated feature flag (AppConfig-backed on prod, in-file default off-prod), gated by org id, user id, or admin
2
+
description: Add a runtime gated feature flag (AppConfig-backed on prod, secret fallback off-prod), gated by org id, user id, or admin
3
3
argument-hint: <flag-name>
4
4
---
5
5
6
6
# Add Feature Flag Skill
7
7
8
-
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), falling back to an in-file default everywhere else.
8
+
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).
9
9
10
10
## When to use this vs `env-flags.ts`
11
11
@@ -16,7 +16,7 @@ If the user wants a fixed per-deployment toggle, send them to `env-flags.ts` ins
16
16
17
17
## The flag model
18
18
19
-
A flag is a named rule in `apps/sim/lib/core/config/feature-flags.ts`. It is ON for a context when **any** clause matches:
19
+
A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any clause matches:
20
20
21
21
```ts
22
22
interfaceFeatureFlagRule {
@@ -27,19 +27,19 @@ interface FeatureFlagRule {
27
27
}
28
28
```
29
29
30
+
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.
31
+
30
32
## Steps
31
33
32
-
1.**Define the default.** Add an entry to `DEFAULT_FEATURE_FLAGS` in `apps/sim/lib/core/config/feature-flags.ts`. This is the source of truth off-AppConfig (self-hosted/OSS, local dev) and documents the intended shape. Use a **kebab-case** key:
34
+
1.**Register the flag.** Add an entry to `FEATURE_FLAG_FALLBACKS` in `apps/sim/lib/core/config/feature-flags.ts`, mapping the flag name (kebab-case) to the secret consulted when AppConfig isn't the source of truth. A truthy secret turns the flag on globally:
33
35
34
36
```ts
35
-
const DEFAULT_FEATURE_FLAGS:FeatureFlagsConfig= {
36
-
flags: {
37
-
'<flag-name>': { admins: true },
38
-
},
37
+
const FEATURE_FLAG_FALLBACKS = {
38
+
'<flag-name>': () =>env.<FLAG_SECRET>,
39
39
}
40
40
```
41
41
42
-
Default conservatively (usually `{ admins: true }` or empty `{}` so it's off for everyone until you roll out).
42
+
Add `<FLAG_SECRET>` to `apps/sim/lib/core/config/env.ts` (and the deployment's secret store). Do **not** add any org/user/admin defaults here — that gating exists only in AppConfig.
43
43
44
44
2.**Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it:
45
45
@@ -55,14 +55,15 @@ interface FeatureFlagRule {
55
55
- Admin routes that already know the caller is an admin may pass `{ userId, isAdmin: true }` to skip the role lookup.
56
56
-**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.
57
57
58
-
3.**(Prod) publish to AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the key under `flags` in the hosted `feature-flags` document and start a `sim-<env>-fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). Until then, prod uses whatever the document already contains; the in-file default applies only when AppConfig is disabled.
58
+
3.**(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag under `flags` in the hosted `feature-flags` document — including any `orgIds`/`userIds`/`admins` gating — and start a `sim-<env>-fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). The fallback secret only applies when AppConfig is disabled.
59
59
60
-
4.**Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts` covering the flag's gating (use the `withAppConfig({ flags: { ... } })`helper; mock `isPlatformAdmin`when the `admins` clause is involved).
60
+
4.**Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts`: use `withAppConfig({ flags: { ... } })`to cover the gating rule (mock `isPlatformAdmin`for the `admins` clause), and toggle the fallback secret to cover the off-AppConfig path.
61
61
62
-
5.**Clean up after rollout.** When the feature ships to everyone, delete the flag from `DEFAULT_FEATURE_FLAGS`, the AppConfig document, the call sites, and the test. Leaving dead flags around is the main failure mode of flag systems.
62
+
5.**Clean up after rollout.** When the feature ships to everyone, delete the flag from `FEATURE_FLAG_FALLBACKS`, 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.
63
63
64
64
## Notes
65
65
66
-
-Tool IDs / flag keys are `kebab-case`.
66
+
-Flag keys are `kebab-case`.
67
67
- Never read flags via raw `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `getFeatureFlags`.
68
+
- Never bake gating into code. The fallback is a single boolean secret; org/user/admin scoping is AppConfig-only.
68
69
- 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 when `admins` is the deciding clause.
Copy file name to clipboardExpand all lines: .cursor/commands/add-feature-flag.md
+13-12Lines changed: 13 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Add Feature Flag Skill
2
2
3
-
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), falling back to an in-file default everywhere else.
3
+
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).
4
4
5
5
## When to use this vs `env-flags.ts`
6
6
@@ -11,7 +11,7 @@ If the user wants a fixed per-deployment toggle, send them to `env-flags.ts` ins
11
11
12
12
## The flag model
13
13
14
-
A flag is a named rule in `apps/sim/lib/core/config/feature-flags.ts`. It is ON for a context when **any** clause matches:
14
+
A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any clause matches:
15
15
16
16
```ts
17
17
interfaceFeatureFlagRule {
@@ -22,19 +22,19 @@ interface FeatureFlagRule {
22
22
}
23
23
```
24
24
25
+
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.
26
+
25
27
## Steps
26
28
27
-
1.**Define the default.** Add an entry to `DEFAULT_FEATURE_FLAGS` in `apps/sim/lib/core/config/feature-flags.ts`. This is the source of truth off-AppConfig (self-hosted/OSS, local dev) and documents the intended shape. Use a **kebab-case** key:
29
+
1.**Register the flag.** Add an entry to `FEATURE_FLAG_FALLBACKS` in `apps/sim/lib/core/config/feature-flags.ts`, mapping the flag name (kebab-case) to the secret consulted when AppConfig isn't the source of truth. A truthy secret turns the flag on globally:
28
30
29
31
```ts
30
-
const DEFAULT_FEATURE_FLAGS:FeatureFlagsConfig= {
31
-
flags: {
32
-
'<flag-name>': { admins: true },
33
-
},
32
+
const FEATURE_FLAG_FALLBACKS = {
33
+
'<flag-name>': () =>env.<FLAG_SECRET>,
34
34
}
35
35
```
36
36
37
-
Default conservatively (usually `{ admins: true }` or empty `{}` so it's off for everyone until you roll out).
37
+
Add `<FLAG_SECRET>` to `apps/sim/lib/core/config/env.ts` (and the deployment's secret store). Do **not** add any org/user/admin defaults here — that gating exists only in AppConfig.
38
38
39
39
2.**Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it:
40
40
@@ -50,14 +50,15 @@ interface FeatureFlagRule {
50
50
- Admin routes that already know the caller is an admin may pass `{ userId, isAdmin: true }` to skip the role lookup.
51
51
-**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.
52
52
53
-
3.**(Prod) publish to AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the key under `flags` in the hosted `feature-flags` document and start a `sim-<env>-fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). Until then, prod uses whatever the document already contains; the in-file default applies only when AppConfig is disabled.
53
+
3.**(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag under `flags` in the hosted `feature-flags` document — including any `orgIds`/`userIds`/`admins` gating — and start a `sim-<env>-fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). The fallback secret only applies when AppConfig is disabled.
54
54
55
-
4.**Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts` covering the flag's gating (use the `withAppConfig({ flags: { ... } })`helper; mock `isPlatformAdmin`when the `admins` clause is involved).
55
+
4.**Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts`: use `withAppConfig({ flags: { ... } })`to cover the gating rule (mock `isPlatformAdmin`for the `admins` clause), and toggle the fallback secret to cover the off-AppConfig path.
56
56
57
-
5.**Clean up after rollout.** When the feature ships to everyone, delete the flag from `DEFAULT_FEATURE_FLAGS`, the AppConfig document, the call sites, and the test. Leaving dead flags around is the main failure mode of flag systems.
57
+
5.**Clean up after rollout.** When the feature ships to everyone, delete the flag from `FEATURE_FLAG_FALLBACKS`, 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.
58
58
59
59
## Notes
60
60
61
-
-Tool IDs / flag keys are `kebab-case`.
61
+
-Flag keys are `kebab-case`.
62
62
- Never read flags via raw `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `getFeatureFlags`.
63
+
- Never bake gating into code. The fallback is a single boolean secret; org/user/admin scoping is AppConfig-only.
63
64
- 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 when `admins` is the deciding clause.
0 commit comments