Skip to content
Back to skills

Feature Switch

ASecurity

Feature switch system guide for gating new user-facing features behind feature flags

  • 1,153 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
ai-agentstypescriptgoreactapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add vm0-ai/vm0 --skill feature-switch --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Feature Switch?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Feature Switch
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vm0-ai-feature-switch/badge)](https://www.skillsdirectory.com/skills/vm0-ai-feature-switch)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: feature-switch
description: Feature switch system guide for gating new user-facing features behind feature flags
---

# Feature Switch Skill

This skill documents the feature switch system and provides step-by-step instructions for adding new feature switches. **All new user-facing features must be gated behind a feature switch** for gradual rollout.

## When to Use

A feature switch is required when adding:

- New UI pages, sections, or sidebar navigation items
- New API endpoints exposed to users or agents
- New integrations (connectors, Slack, Telegram, etc.)
- New agent token capabilities

A feature switch is **not** required for:

- Internal refactors or code cleanup
- Test infrastructure changes
- Build/CI configuration
- Bug fixes to existing features
- Documentation updates

## How to Add a Feature Switch

### Step 1: Add a key to the enum

File: `turbo/packages/core/src/feature-switch-key.ts`

Add a new entry to `FeatureSwitchKey`:

```typescript
export enum FeatureSwitchKey {
  // ... existing keys
  MyFeature = "myFeature",
}
```

### Step 2: Register the switch

File: `turbo/packages/core/src/feature-switch.ts`

Add an entry to the `FEATURE_SWITCHES` record:

```typescript
[FeatureSwitchKey.MyFeature]: {
  maintainer: "you@okou.ai",
  enabled: false,
  enabledOrgIdHashes: STAFF_ORG_ID_HASHES, // optional: staff-only access
},
```

**Configuration options:**

| Field                | Type       | Description                                        |
| -------------------- | ---------- | -------------------------------------------------- |
| `maintainer`         | `string`   | Email of the responsible person                    |
| `enabled`            | `boolean`  | `true` = on for everyone, `false` = off by default |
| `enabledUserHashes`  | `string[]` | FNV-1a hashes of allowed user IDs                  |
| `enabledEmailHashes` | `string[]` | FNV-1a hashes of allowed emails                    |
| `enabledOrgIdHashes` | `string[]` | FNV-1a hashes of allowed org IDs                   |

**Common default states:**

- `enabled: false` — fully hidden unless enabled by a per-user API override
- `enabled: false` + `enabledOrgIdHashes: STAFF_ORG_ID_HASHES` — staff-only (most common for new features)
- `enabled: true` — on for everyone (use when feature is ready for GA)

### Step 3: Gate the feature in application code

Choose the pattern that matches where your feature is consumed.

#### Server-side (API routes)

```typescript
import { isFeatureEnabled, FeatureSwitchKey } from "@okouai/core";

// In route handler:
if (!isFeatureEnabled(FeatureSwitchKey.MyFeature, { userId, orgId })) {
  return createErrorResponse("FORBIDDEN", "Feature not available");
}
```

#### Client-side (Platform UI)

```typescript
import { FeatureSwitchKey } from "@okouai/core";
import { featureSwitch$ } from "../../signals/external/feature-switch.ts";

// In component:
const features = useLastResolved(featureSwitch$);
const showMyFeature = features?.[FeatureSwitchKey.MyFeature] ?? false;

// Conditional rendering:
{showMyFeature && <MyFeatureComponent />}
```

#### Sidebar navigation gating

Sidebar entries are not gated declaratively. The nav item records in
`turbo/apps/platform/src/views/okou-page/sidebar.tsx` (`MANAGE_NAV`,
`FOOTER_NAV`) carry no feature-switch field. Read `featureSwitch$` in the
component that renders the entry and omit the item when the switch is off —
`turbo/apps/platform/src/views/okou-page/sidebar-account.tsx` gates the Lab
entry this way:

```typescript
const features = useLastResolved(featureSwitch$);
const labEnabled = features?.[FeatureSwitchKey.Lab] ?? false;

// In the render:
{labEnabled && <DropdownMenuItem>{/* ... */}</DropdownMenuItem>}
```

#### Connector gating

Connector definitions live in `okou-ai/okou-connectors`, but the platform owns
the rollout association. Add an entry to `FEATURE_SWITCH_BY_AUTH_METHOD` in
`turbo/apps/api/src/signals/services/connector-auth-method-feature-switches.ts`,
keyed by `` `${connectorSlug}\0${authMethodId}` ``:

```typescript
const FEATURE_SWITCH_BY_AUTH_METHOD = Object.freeze<
  Record<string, FeatureSwitchKey | undefined>
>({
  // ... existing entries
  "my-connector\0oauth": FeatureSwitchKey.MyConnector,
});
```

Deploy the association before publishing a method that should be gated, and
remove it once the switch graduates.

#### Agent token capability gating

In `turbo/apps/api/src/signals/auth/tokens.ts`, add to `CONDITIONAL_CAPABILITIES`:

```typescript
const CONDITIONAL_CAPABILITIES = [
  // ... existing entries
  ["my-feature:write", FeatureSwitchKey.MyFeature],
] as const satisfies readonly (readonly [Capability, FeatureSwitchKey])[];
```

## Key Files

| File                                                                            | Role                                                        |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `turbo/packages/core/src/feature-switch-key.ts`                                 | Enum of all feature switch keys                             |
| `turbo/packages/core/src/feature-switch.ts`                                     | Registry and evaluation logic                               |
| `turbo/apps/platform/src/signals/external/feature-switch.ts`                    | Client-side reactive state with override layers             |
| `turbo/apps/platform/src/views/okou-page/sidebar.tsx`                           | Sidebar nav item records (`MANAGE_NAV`, `FOOTER_NAV`)       |
| `turbo/apps/api/src/signals/services/connector-auth-method-feature-switches.ts` | Connector auth-method → feature switch rollout associations |
| `turbo/apps/api/src/signals/services/feature-switches.service.ts`               | Override loading/writing and the org-scoped key list        |
| `turbo/apps/api/src/signals/auth/tokens.ts`                                     | Token capability gating                                     |

## Override Layers

Evaluation has two layers (lowest to highest priority):

1. **Core registry** — static config in source code, evaluated against `userId` / `email` / `orgId` hashes.
2. **DB overrides** — most switches are per-user rows in
   `user_feature_switches` keyed by `(orgId, userId)`. Some switches are
   org-scoped and stored under the org sentinel user id (`ORG_SENTINEL_USER_ID`,
   `"__org__"`); `ORG_SCOPED_FEATURE_SWITCH_KEYS` currently holds
   `LarkIntegration`.
   Written
   via `POST /api/feature-switches` and cleared via
   `DELETE /api/feature-switches`. The Lab page lets users update or reset these
   overrides and groups the registry into Released, Beta, Alpha, and Internal
   categories. Switch keys beginning with `_` are Internal regardless of their
   rollout audience.

The same two-layer resolution applies on the server: route handlers that call
`isFeatureEnabled(..., { userId, orgId, overrides })` pass a context built by
`loadUserFeatureSwitchContext(db, orgId, userId)`.

There is **no** client-only or device-local override layer.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…