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 theexecutive,ai,keithandfeedbackroute 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
| Key | Gates |
|---|---|
executive | The AI Executive view (/executive) |
keith | The Keith assistant (chat + voice) |
global_search | The universal top-bar search (UI gate; its backend search is permission-gated) |
ai | All in-context AI assists and AI survey generation |
feedback | In-app product feedback (launcher + triage) |
signups_enabled | First-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.