Add feature_flags module with tenant overrides, simple consumer API - #44
Merged
Conversation
Persists per-flag overrides in DB on top of the framework's in-memory FeatureFlagRegistry. Service mirrors DB rows to the registry at boot (on_startup hydration) and on every mutation, so is_enabled() stays a plain in-process lookup with no per-request DB hit. REST API + Inertia admin page lock behind feature_flags.view / feature_flags.manage. Stale overrides (for flags no module registers anymore) are hidden from list_flags() but kept in the DB so reintroducing the flag picks them up.
Flags can now be overridden at two scopes: * system — applies to every request (the previous default behavior) * tenant — applies only when is_enabled() is called with a matching tenant_id; per-tenant value beats the system value Resolution at runtime: tenant override > system override > definition default. The override table grows a (scope, scope_id) pair with composite unique on (scope, scope_id, name); SCOPE_TENANT rows store the tenant id in scope_id, SCOPE_SYSTEM rows use the empty string. The registry holds two maps (system, tenant) so is_enabled() stays O(1). The admin UI gains a tenant selector and shows the inherited system value next to each tenant-scoped toggle so it's obvious what clearing the override would revert to.
list_flags now reads override state from the in-memory registry instead of refetching the overrides table on every admin page load. set_override skips no-op writes and the redundant post-flush refresh; API write handlers build the response view directly via service.build_view rather than re-listing all flags. Also aligns IFeatureFlagService with the concrete service signatures and trims narrative comments and section dividers.
Follows the rule documented in #39: ship a service Protocol only for real extension points. Nothing outside feature_flags imports the interface; deps.py wires the concrete service and endpoints type-hint against FeatureFlagService directly.
Consumers previously had to fetch the registry off request.app.state.sm, read request.state.tenant_id, and call is_enabled with both — four lines of ceremony for a single check. Add three helpers in simple_module_core.feature_flags: - is_flag_enabled(request, name): tenant-aware check in one line - flag_enabled(name): FastAPI dep factory yielding bool - require_flag(name): FastAPI dep factory that 404s when off All three resolve tenant via request.state.tenant_id set by TenantMiddleware, so consumers never have to thread tenant_id manually. Living in core (not the feature_flags module) keeps plugin consumers from taking a runtime dependency on the admin UI module. Document the three patterns in docs/module-authoring.md.
Flag checks no longer force consumers to retype the registered flag name. All four helpers — is_flag_enabled, flag_enabled, require_flag, and the new feature_flag decorator — accept either a FeatureFlagDefinition constant (preferred) or the raw string name. The new @feature_flag(FLAG) decorator is an attribute-style alternative to dependencies=[Depends(require_flag(...))]; apply it directly to an endpoint handler that declares a request: Request parameter. The decorator validates the signature at import time so a misconfigured handler fails fast instead of 500-ing at request time. https://claude.ai/code/session_01AKxpW8EjmzJHSfrsQWyoqn
…ure-flags-ZlNqP # Conflicts: # package-lock.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
feature_flagsadmin module: DB-persisted overrides layered on top of the in-memoryFeatureFlagRegistry, with a two-scope resolution order (tenant → system → definition default), REST + Inertia admin UI, and hydration on startup.is_flag_enabled,flag_enabled,require_flag, and a new attribute-style@feature_flag(FLAG)decorator — all tenant-aware and all accepting either aFeatureFlagDefinitionconstant or the raw name, so callers never retype flag strings.docs/module-authoring.mddocuments the module-level constant pattern and all four consumption styles; includes one worked example per style.Test plan
make lint— ruff format-check, ruff, ty, biome, tsc across all workspaces, 300-line file-size cap, hardcoded-string checkmake test-py— 753 passed (adds 30 new tests: registry + tenant overrides + consumer helpers +@feature_flagdecorator incl. sync handler and missing-Request-param TypeError)make test-js— 8 passed@feature_flag(FLAG), toggle from the admin UI, confirm per-tenant override wins over system overridehttps://claude.ai/code/session_01AKxpW8EjmzJHSfrsQWyoqn