Skip to content

feature_flags ​

Runtime feature toggles. Modules register named flags with a default value; an admin can override them per-system or per-tenant from the UI; service code asks the in-memory registry whether a flag is on at request time. Resolution precedence: tenant override → system override → registered default.

ModuleMeta ​

FieldValue
nameFeatureFlags
route_prefix/api/feature_flags
view_prefix/admin/feature-flags
depends_on(none)

Registering a flag ​

Anywhere a module has access to app.state.sm.feature_flags (typically inside register_feature_flags):

python
from simple_module_core.feature_flags import FeatureFlag


class OrdersModule(ModuleBase):
    def register_feature_flags(self, registry):
        registry.register(
            FeatureFlag(
                name="orders.beta_checkout",
                description="Enable the new checkout flow",
                default_enabled=False,
            )
        )

Once registered, the flag shows up at /admin/feature-flags in the admin UI. Unregistered names return 404 from the override endpoints — flags are explicit, not free-form keys.

Reading a flag at request time ​

python
from feature_flags.deps import FeatureFlagRegistryDep


@router.post("/checkout")
async def checkout(
    body: CheckoutRequest,
    flags: FeatureFlagRegistryDep,
    user: CurrentUser,
) -> ...:
    if flags.is_enabled("orders.beta_checkout", tenant_id=user.tenant_id):
        return await new_checkout(body, user)
    return await legacy_checkout(body, user)

is_enabled(name, tenant_id=None) resolves precedence and returns a bool. The registry is in-memory and hydrated at boot from the feature_flags_override table — no DB round-trip per call.

Routes ​

API ​

All require authentication. Read endpoints need feature_flags.view; write endpoints need feature_flags.manage.

Method + pathBody / responsePermission
GET /api/feature_flags/→ list[FeatureFlagView]feature_flags.view
GET /api/feature_flags/{name}→ FeatureFlagViewfeature_flags.view
PUT /api/feature_flags/{name}ToggleRequest → FeatureFlagViewfeature_flags.manage
DELETE /api/feature_flags/{name}→ 204 (clear system override)feature_flags.manage
GET /api/feature_flags/tenant/{tenant_id}→ list[FeatureFlagView]feature_flags.view
PUT /api/feature_flags/tenant/{tenant_id}/{name}ToggleRequest → FeatureFlagViewfeature_flags.manage
DELETE /api/feature_flags/tenant/{tenant_id}/{name}→ 204feature_flags.manage

View ​

Method + pathInertia componentPermission
GET /admin/feature-flags/FeatureFlags/Browse (optional ?tenant_id=...)feature_flags.view
POST /admin/feature-flags/{name}/toggleredirectfeature_flags.manage
POST /admin/feature-flags/{name}/clearredirectfeature_flags.manage

Public contracts ​

python
from feature_flags.contracts.schemas import FeatureFlagView, ToggleRequest
ClassPurpose
FeatureFlagViewWhat the API + admin UI returns: name, description, default_enabled, enabled (resolved), overridden (does this scope have an override?), system_enabled (only for tenant scope).
FeatureFlagOverrideOutThe persisted override row.
ToggleRequest{ "enabled": bool }.

Models ​

FeatureFlagOverride (table feature_flags_override)

ColumnTypeNotes
idintPK
scopestr(10)"system" or "tenant"
scope_idstr(64)"" for system, the tenant id otherwise
namestr(200)flag name
enabledboolthe override value
created_at / updated_atdatetimefrom AuditMixin

Unique constraint on (scope, scope_id, name). The scope_id="" (instead of NULL) sidesteps PostgreSQL's "NULLs are distinct in UNIQUE indexes" rule.

Permissions ​

CodeGranted toPurpose
feature_flags.viewadminread flags + overrides
feature_flags.manageadmincreate / update / delete overrides
LabelURLIconSectionGroupOrder
Feature Flags/admin/feature-flagsflagADMIN_SIDEBARSystem110

Inertia pages ​

  • FeatureFlags/Browse.tsx — single browse page with a system/tenant scope switcher, table of all registered flags, and toggle / clear-override actions.

Locales ​

Top-level keys in feature_flags/locales/en.json:

  • browse — page strings (title, description, empty state, count plurals, scope switcher).
  • table — column headers and badges (enabled, disabled, overridden, following_default, following_system, system_value, clear_override).
  • toasts — notifications (enabled, disabled, cleared, toggle_failed).

Notes ​

  • The FeatureFlagService keeps the in-memory registry in sync after every DB mutation, so is_enabled() is always cheap.
  • Tenant-scope endpoints use a non-empty scope_id; the system scope uses "". Don't pass None — the unique index treats NULL as distinct on Postgres and you'll end up with duplicate rows.
  • The Browse page lists tenants that have any override attached, so you can jump from system view → a tenant view that actually has overrides without typing IDs.

Tenant deletion durably removes persisted tenant overrides and clears the local registry through a TenantDeleted subscriber (#639, #698). Workers also forget the tenant on the existing tenants.deleted invalidation broadcast; system and other tenants' overrides are preserved, including overrides for unknown flags. The periodic override reload remains the fallback for missed broadcasts after durable cleanup completes.

Released under the MIT License.