Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6806fc2
fix(resources): sort folders and resources as one list so pinned item…
waleedlatif1 Aug 3, 2026
806ea0c
fix(tooltip): stop text blurring for ~150ms every time a tooltip appe…
waleedlatif1 Aug 3, 2026
0bc4fb4
fix(security): meter and throttle the deployed-chat TTS relay (#6212)
waleedlatif1 Aug 3, 2026
dd79515
fix(env): restore beforeInteractive on the hosted public env script (…
waleedlatif1 Aug 3, 2026
78740c0
chore(chat): remove deployed-chat voice mode (#6215)
waleedlatif1 Aug 3, 2026
83988c1
refactor(chat): drop code the voice removal left unreachable (#6218)
waleedlatif1 Aug 3, 2026
09eba8a
improvement(ci): move CodeQL off default setup onto Blacksmith (#6219)
waleedlatif1 Aug 3, 2026
36aace7
fix(auth): correct callback URL resolution across SSR and hydration (…
waleedlatif1 Aug 3, 2026
b0491f1
fix(chat): invalidate deployment queries after a chat mutation (#6223)
waleedlatif1 Aug 3, 2026
1708173
refactor(chat): clean up the deployed chat surface (#6220)
waleedlatif1 Aug 3, 2026
3f70096
improvement(self-host): gate email verification on a mail provider, a…
TheodoreSpeaks Aug 3, 2026
c759a88
refactor(voice): load STT availability through React Query (#6224)
waleedlatif1 Aug 3, 2026
67e355b
chore(landing): remove the Russell Investments logo (#6228)
waleedlatif1 Aug 3, 2026
1242301
improvement(emcn): share one emails/domains chip input across share a…
waleedlatif1 Aug 3, 2026
3de63c9
feat(self-host): align Docker Compose with Helm and overhaul self-hos…
waleedlatif1 Aug 3, 2026
5ab5f2c
feat(browser, terminal): implement browser driver, password manager, …
Sg312 Aug 3, 2026
41592df
fix(execution): stop the event buffer retaining a run-length backlog …
waleedlatif1 Aug 3, 2026
f210a6e
fix(execution): offload buffered event values under budget pressure (…
waleedlatif1 Aug 4, 2026
ed17bb2
chore(deps): upgrade next to 16.3.0 and clean up the TypeScript toolc…
waleedlatif1 Aug 4, 2026
d05289c
fix(providers): route 6-10MB attachments to the provider large-file p…
waleedlatif1 Aug 4, 2026
8940833
improvement(agent): allow variable references in reasoning effort, ve…
waleedlatif1 Aug 4, 2026
856fe0f
fix(docker): upgrade bun to 1.3.14 (#6236)
waleedlatif1 Aug 4, 2026
40cbee4
improvement(skills): ask for feature flag granularity (#6238)
TheodoreSpeaks Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
46 changes: 34 additions & 12 deletions .agents/skills/add-feature-flag/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
---
name: add-feature-flag
description: Add a runtime gated feature flag (AppConfig-backed on prod, secret fallback off-prod), gated by org id, user id, or admin
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, 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).
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`): per-request, gated by `userId`/`orgId`/admin, changeable at runtime. This skill.
- **Feature flag** (`@/lib/core/config/feature-flags.ts`): runtime global on/off by default, optionally scoped by `userId`/`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 clause matches:
A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any configured clause matches:

```ts
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')
adminEnabled?: 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.
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

1. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally):
1. **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.ts` instead.

2. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally):

```ts
const FEATURE_FLAGS = {
Expand All @@ -45,7 +53,19 @@ Critically, **none of this is expressible in code** — gating (especially `admi

`fallback` is the env/secret key (typed as `keyof typeof env`), so add `<FLAG_SECRET>` to `apps/sim/lib/core/config/env.ts` first (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 valid `FeatureFlagName`.

2. **Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it:
3. **Gate the call site at the chosen granularity.** For the recommended global mode, pass no context:

```ts
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:

```ts
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
Expand All @@ -55,19 +75,21 @@ Critically, **none of this is expressible in code** — gating (especially `admi
}
```

- Organization targeting uses `orgId`; user and platform-admin targeting require `userId`.
- Missing ids are fine — a clause with no matching id is skipped; with no `userId`, the admin clause resolves to `false` without 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.

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.
4. **(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag to the hosted `feature-flags` document using `enabled` for global rollout or only the selected `orgIds`/`userIds`/`adminEnabled` clauses for scoped rollout, then 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.

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.
5. **Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts` that matches the chosen granularity. For a global flag, exercise `isFeatureEnabled('<flag-name>')` with an AppConfig `enabled` rule and toggle the fallback secret for the off-AppConfig path. For scoped rollout, cover only the selected clauses and mock `isPlatformAdmin` when testing `adminEnabled`.

5. **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.
6. **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 `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `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 when `admins` is the deciding clause.
- 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 when `adminEnabled` is the deciding clause.
46 changes: 34 additions & 12 deletions .claude/commands/add-feature-flag.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
---
description: Add a runtime gated feature flag (AppConfig-backed on prod, secret fallback off-prod), gated by org id, user id, or admin
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, 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).
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`): per-request, gated by `userId`/`orgId`/admin, changeable at runtime. This skill.
- **Feature flag** (`@/lib/core/config/feature-flags.ts`): runtime global on/off by default, optionally scoped by `userId`/`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 clause matches:
A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any configured clause matches:

```ts
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')
adminEnabled?: 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.
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

1. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally):
1. **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.ts` instead.

2. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally):

```ts
const FEATURE_FLAGS = {
Expand All @@ -44,7 +52,19 @@ Critically, **none of this is expressible in code** — gating (especially `admi

`fallback` is the env/secret key (typed as `keyof typeof env`), so add `<FLAG_SECRET>` to `apps/sim/lib/core/config/env.ts` first (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 valid `FeatureFlagName`.

2. **Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it:
3. **Gate the call site at the chosen granularity.** For the recommended global mode, pass no context:

```ts
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:

```ts
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
Expand All @@ -54,19 +74,21 @@ Critically, **none of this is expressible in code** — gating (especially `admi
}
```

- Organization targeting uses `orgId`; user and platform-admin targeting require `userId`.
- Missing ids are fine — a clause with no matching id is skipped; with no `userId`, the admin clause resolves to `false` without 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.

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.
4. **(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag to the hosted `feature-flags` document using `enabled` for global rollout or only the selected `orgIds`/`userIds`/`adminEnabled` clauses for scoped rollout, then 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.

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.
5. **Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts` that matches the chosen granularity. For a global flag, exercise `isFeatureEnabled('<flag-name>')` with an AppConfig `enabled` rule and toggle the fallback secret for the off-AppConfig path. For scoped rollout, cover only the selected clauses and mock `isPlatformAdmin` when testing `adminEnabled`.

5. **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.
6. **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 `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `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 when `admins` is the deciding clause.
- 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 when `adminEnabled` is the deciding clause.
2 changes: 1 addition & 1 deletion .claude/rules/sim-url-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const { sort, dir, activeSort, onSort, onClear } = useUrlSort(thingsSortParams,
Two modes, chosen by whether you pass a default:

- **Defaulted (the common case)** — pass the list's existing default sort; it must match exactly. A clean URL means the default ordering; explicitly selecting the default collapses back to a clean URL (`clearOnDefault`), and "clear sort" writes the defaults back. `useUrlSort` derives `activeSort: null` for the default state.
- **Nullable** — omit the default when "no active sort" is behaviorally distinct from explicitly sorting by the fallback column (e.g. files: with no sort, files order by updated/desc but folders by name/asc). The params carry no defaults, explicit selections always persist in the URL, and "clear sort" strips both params (`useUrlSort` writes `null`s).
- **Nullable** — omit the default when "no active sort" is behaviorally distinct from explicitly sorting by the fallback column (e.g. document chunks: with no sort the query omits `sortBy` entirely and the server's own order applies). The params carry no defaults, explicit selections always persist in the URL, and "clear sort" strips both params (`useUrlSort` writes `null`s).

Sort params live alongside — not inside — the feature's grouped filter parser map (one definition per param; `useUrlSort` owns its own `useQueryStates`, and nuqs keeps hooks on the same keys in sync). Both params carry the shared filter options (`{ history: 'replace', clearOnDefault: true }`). Free-form user-defined columns (e.g. `tables/[tableId]`) can't use `parseAsStringLiteral` and stay hand-rolled with `parseAsString` — reuse the shared `SORT_DIRECTIONS` there.

Expand Down
Loading
Loading