Feature Flags
Operator-level toggles for features that aren't ready for general exposure yet. Lives in AppSettings.Features — not per-tenant. The operator (whoever sets the deployment's configuration) decides; realm admins can't override.
Why operator-level
Some features ship with the editor side functional but the runtime side still missing, or with a beta integration where we want to gather real feedback before exposing it. The flag keeps the surface invisible to tenant admins until the operator is comfortable; once flipped, the feature behaves as documented on its own page.
Setting flags
Configure via configuration.local.json (gitignored) or an environment variable. Casing is case-sensitive — see the convention note below.
// configuration.local.json
{
"AppSettings": {
"Features": {
"PageBuilder": true
}
}
}# or via env (PascalCase, double-underscore as section separator):
AppSettings__Features__PageBuilder=trueFlags are read at startup; no hot-reload. A flip requires a restart.
Current flags
| Flag | Default | Effect |
|---|---|---|
PageBuilder | false | Visibility of the Customization → Pages editor. While off: sidebar tile hidden, /plattform/customization/pages routes redirect to Branding, /api/admin/customization/pages/* returns 404, RealmSettingsDto omits the Pages section. While on: editor mounts and persists. Runtime rendering of stored schemas on /login etc. is a separate sprint and is not gated by this flag. |
Defense in depth
For each gated feature the flag fires at every layer the surface touches:
- SPA sidebar — the navigation entry is hidden via
requireFeatureinAdminView.vue. - Vue-router —
beforeEnterguards on the gated routes redirect to a visible sibling so deep-links don't dead-end on a blank screen. - Backend endpoints — return 404 Not Found (not 403 / 401) so curl-callers see "no such endpoint", not "permission denied".
- DTO masking — surfaces that aggregate multiple sub-documents (e.g.
GET /api/admin/realm-settings) emit the gated section as empty so the SPA can't fingerprint stored data.
The stored data itself persists across flips — turning a flag off doesn't delete anything from the tenant DB. Flipping it back on surfaces the existing data unchanged.
ENV-var casing
Cocoar.Configuration v5 (the binding layer Modgud uses) reads environment variables case-sensitively with the same shape as the JSON keys. PascalCase only — AppSettings__Features__PageBuilder=true, never APPSETTINGS__FEATURES__PAGEBUILDER. The latter is silently ignored.
This applies to every config-bound type, not just feature flags. The same rule covers DbSettings__ConnectionString, Observability__Prometheus__BearerToken, etc.
Adding a flag (developer note)
For repository contributors — flags follow a deliberate pattern:
- Add a property to
Modgud.Api.FeatureFlagswith afalsedefault. - The
IFeatureFlagsabstraction inModgud.Authenticationmirrors the property (read-only) so the Authentication slice can gate surfaces without depending on the Api project. - Wire it everywhere: sidebar
requireFeature(with a matching'PageBuilder' | …union member inNavItem), Vue-routerbeforeEnterguard, backend endpoint 404 short-circuit, DTO masking. - Add tests in
Modgud.Api.Tests/Authorization/covering on/off paths. - Document the flag in this file plus its feature page.
Don't add a flag for "I'm not sure if I want this enabled" — flags are commitment-eating maintenance work. Add them only when there's a concrete reason the feature isn't ready for general exposure (beta integration, half-built runtime, customer-specific).