Skip to main content

Feature flags & gating

Feature flags hide a feature from everyone except system admins until it's released. They are global (platform-wide, not per-tenant) and live in the system.feature_flag table (non-RLS — it's cross-tenant admin config, not tenant data).

The rule

Everywhere — backend and front end — the gate is:

available = flagEnabled || isSystemAdmin

A system admin (the cross-tenant operator role) always previews a flagged feature. A new gated feature defaults OFF, so it's admin-only until a system admin turns it on.

Backend

  • Read a flag: db.isFeatureEnabled(key, fallback) (owner connection).
  • Gate a whole route group: featureGate(key) middleware (api/src/middleware/feature-flag.ts) — applied to the executive, ai, keith and feedback route groups. For a single route inside a mixed group (e.g. POST /surveys/generate), check inline.
  • Toggle: PUT /v1/system/features/:key (system-admin only); list: GET /v1/system/features.

Front end

GET /v1/me returns a features map ({ key: enabled }). usePermissions() exposes it and a feature(key) helper that applies the sysadmin bypass; it's cached in localStorage alongside permissions. Gate UI with {perms.feature('keith') && <KeithWidget/>}, route predicates (p) => p.feature('executive') && p.can('executive.view'), etc. The AI kit (AiButton/AiLauncher) self-gates on ai.

Current flags

KeyGates
executiveThe AI Executive view (/executive)
keithThe Keith assistant (chat + voice)
global_searchThe universal top-bar search (UI gate; its backend search is permission-gated)
aiAll in-context AI assists and AI survey generation
feedbackIn-app product feedback (launcher + triage)
signups_enabledFirst-time-sign-in self-provisioning of a new workspace

Seeded OFF for the gated set (migration 0096); manage them in the system-admin Features screen.