Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Feature switch system guide for gating new user-facing features behind feature flags
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-01 | ✗→✓ | ▲ Improved | -6% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -6% | 0% |
| case-04 | ✗→✓ | ▲ Improved | -14% | 0% |
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.
A feature switch is required when adding:
A feature switch is not required for:
File: turbo/packages/core/src/feature-switch-key.ts
Add a new entry to FeatureSwitchKey:
typescriptexport enum FeatureSwitchKey { // ... existing keys MyFeature = "myFeature", }
File: turbo/packages/core/src/feature-switch.ts
Add an entry to the FEATURE_SWITCHES record:
typescript[FeatureSwitchKey.MyFeature]: { maintainer: "you@vm0.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 until manually enabled via Lab pageenabled: 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)Choose the pattern that matches where your feature is consumed.
typescriptimport { isFeatureEnabled, FeatureSwitchKey } from "@vm0/core"; // In route handler: if (!isFeatureEnabled(FeatureSwitchKey.MyFeature, { userId, orgId })) { return createErrorResponse("FORBIDDEN", "Feature not available"); }
typescriptimport { FeatureSwitchKey } from "@vm0/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 />}
In turbo/apps/platform/src/views/zero-page/zero-sidebar.tsx, add a featureGate to the sidebar item:
typescript{ id: "my-feature", label: "My Feature", icon: MyIcon, featureGate: FeatureSwitchKey.MyFeature, }
In turbo/packages/core/src/contracts/connectors.ts, add featureFlag to the connector config:
typescriptmyConnector: { label: "My Connector", featureFlag: FeatureSwitchKey.MyConnector, // ... }
In turbo/apps/api/src/signals/auth/tokens.ts, add to CONDITIONAL_CAPABILITIES:
typescriptconst CONDITIONAL_CAPABILITIES: ReadonlyMap<ZeroCapability, FeatureSwitchKey> = new Map([ // ... existing entries ["my-feature:write", FeatureSwitchKey.MyFeature], ]);
| 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/zero-page/zero-sidebar.tsx | Sidebar nav items with featureGate | | turbo/apps/api/src/signals/services/connector-catalog-artifacts/source.ts | External connector auth-method schema with featureSwitch field | | turbo/apps/api/src/signals/auth/tokens.ts | Token capability gating |
Evaluation has two layers (lowest to highest priority):
userId / email / orgId hashes.user_feature_switches keyed by (orgId, userId). Some switches are org-scoped and stored under the org sentinel user id (__org__); currently ChatErrorRecovery and PiLoop are org-scoped. Written via the Lab page toggles or window._vm0.featureSwitches.myFeature = true (both call POST /api/zero/feature-switches). Cleared via the Lab page "Reset all" button (DELETE /api/zero/feature-switches).
The same two-layer resolution applies on the server: route handlers that call isFeatureEnabled(..., { userId, orgId, overrides }) pass overrides loaded via loadFeatureSwitchOverrides(orgId, userId).
There is no client-only layer. window._vm0.featureSwitches requires auth and persists across refreshes; there is no device-local override.
Other measured skills in the registry, with their headline benchmark lift.