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.
::: info 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. :::
Configure via configuration.local.json (gitignored) or an environment variable. Env-var binding is case-insensitive — see the convention note below.
# or via env (double-underscore as section separator; casing is not significant):
AppSettings__Features__PageBuilder=trueFlags are read at startup; no hot-reload. A flip requires a restart.
| 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. |
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.
Cocoar.Configuration v6 (the binding layer Modgud uses) binds environment variables case-insensitively. The section/property names need not match the JSON or C# casing — AppSettings__Features__PageBuilder=true and APPSETTINGS__FEATURES__PAGEBUILDER=true bind to the same flag. Two underscores (__) are the section separator; a single underscore is literal. PascalCase is a readability convention only, not a correctness requirement.
This applies to every config-bound type, not just feature flags. The same rule covers DbSettings__ConnectionString, Observability__Prometheus__BearerToken, etc.
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).