All new user-facing features must be gated behind a feature flag (ADR-0007). This guide walks through the end-to-end process.
- Choose a flag name following the naming convention
- Register the flag in
flags.json(ormodels.jsonfor model flags) - Gate the backend handler
- Gate the frontend component
- Decide visibility: platform-only or workspace-configurable
- Test with Unleash disabled (verify fail-closed behavior)
Follow the <component>.<feature>.<aspect> convention:
| Category | Pattern | Example | Fail Mode |
|---|---|---|---|
| General | <component>.<feature>.<aspect> |
frontend.file-explorer.enabled |
Fail-closed |
| Runner | runner.<runnerId>.enabled |
runner.gemini-cli.enabled |
Fail-closed |
| Model | model.<modelId>.enabled |
model.claude-opus-4-6.enabled |
Fail-open |
General and runner flags default to off when Unleash is unavailable (fail-closed). Model flags default to on (fail-open) so model availability is never blocked by flag infrastructure outages.
Add an entry to components/manifests/base/core/flags.json:
{
"flags": [
{
"name": "frontend.my-feature.enabled",
"description": "Enable the my-feature UI for session creation",
"tags": [
{
"type": "scope",
"value": "workspace"
}
]
}
]
}Omit the tags array to make the flag platform-only (not visible in the workspace admin UI).
Model flags are auto-generated from components/manifests/base/core/models.json at startup. Set "featureGated": true on the model entry. No flags.json entry is needed.
The backend syncs flags.json and models.json to Unleash on boot (cmd/sync_flags.go). New flags are created automatically; flags for removed models are archived. The sync requires UNLEASH_ADMIN_URL and UNLEASH_ADMIN_TOKEN and skips silently if they are not set.
Use the handler-level wrappers in handlers/featureflags.go. Choose the right function based on your use case:
import "net/http"
// Option A: Hide the feature entirely (404 when disabled)
if !handlers.FeatureEnabled("frontend.my-feature.enabled") {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
// Option B: Branch behavior (legacy vs new)
if handlers.FeatureEnabled("frontend.my-feature.enabled") {
handleNewBehavior(c)
} else {
handleLegacyBehavior(c)
}
// Option C: Per-user rollout (Unleash strategies with user context)
if !handlers.FeatureEnabledForRequest(c, "frontend.my-feature.enabled") {
c.JSON(http.StatusForbidden, gin.H{"error": "feature not enabled for you"})
return
}| Function | Fail Mode | When to Use |
|---|---|---|
handlers.FeatureEnabled(flag) |
Fail-closed | Same result for all users |
handlers.FeatureEnabledForRequest(c, flag) |
Fail-closed | Per-user rollout, A/B tests |
featureflags.IsModelEnabled(flag) |
Fail-open | Model availability checks |
components/backend/featureflags/featureflags.go— SDK init,IsEnabled,IsModelEnabledcomponents/backend/handlers/featureflags.go— handler wrappers
For flags evaluated purely in the browser via the Unleash React SDK:
import { useFlag } from '@/lib/feature-flags';
export function MyComponent() {
const enabled = useFlag('frontend.my-feature.enabled');
if (!enabled) return null;
return <NewFeature />;
}For flags that respect workspace ConfigMap overrides:
import { useWorkspaceFlag } from '@/services/queries/use-feature-flags-admin';
export function MyComponent({ projectName }: { projectName: string }) {
const { enabled, isLoading } = useWorkspaceFlag(projectName, 'frontend.my-feature.enabled');
if (isLoading) return <Spinner />;
if (!enabled) return null;
return <NewFeature />;
}Use useWorkspaceFlag when workspace admins should be able to override the flag independently.
| Visibility | Unleash Tag | Who Controls | Use When |
|---|---|---|---|
| Workspace-configurable | scope: workspace |
Workspace admins + Platform team | Beta opt-in, experimental UI |
| Platform-only | (no tag) | Platform team only | Infrastructure, security, kill switches |
To make a flag workspace-configurable, include the tag in flags.json (shown in step 2) or add it manually in the Unleash UI: open the flag > Tags > add type scope, value workspace.
When UNLEASH_URL is not set, the SDK is not initialized:
- General flags →
false(fail-closed). Your feature should be hidden. - Model flags →
true(fail-open). Models should be available.
Run through the UI and verify the gated feature is not visible.
make deploy-unleash-kind # Deploy Unleash to Kind cluster
make unleash-port-forward # Access at http://localhost:4242Then toggle the flag in the Unleash UI and verify the feature turns on/off without a redeploy.
- Stop or remove the Unleash deployment
- Restart the backend
- Confirm your feature is hidden (general flags) or available (model flags)
When a flag is evaluated for a workspace, three layers are checked in order:
- Workspace ConfigMap override (highest priority) —
feature-flag-overridesConfigMap in the workspace namespace - Unleash SDK evaluation — respects strategies, rollout percentages, A/B tests
- Code default (lowest priority) — general:
false, model:true
See Fail Modes Reference for the full matrix.
- Feature Flags Overview — Unleash integration, admin UI, API endpoints
- Fail Modes Reference — fail-open vs fail-closed details
- ADR-0007 — architectural decision and rationale