Skip to content

Add feature_flags module with tenant overrides, simple consumer API - #44

Merged
antosubash merged 7 commits into
mainfrom
claude/implement-feature-flags-ZlNqP
Apr 20, 2026
Merged

Add feature_flags module with tenant overrides, simple consumer API#44
antosubash merged 7 commits into
mainfrom
claude/implement-feature-flags-ZlNqP

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

  • Ships a feature_flags admin module: DB-persisted overrides layered on top of the in-memory FeatureFlagRegistry, with a two-scope resolution order (tenant → system → definition default), REST + Inertia admin UI, and hydration on startup.
  • Consumer API is a one-liner, framework-level (no plugin coupling): is_flag_enabled, flag_enabled, require_flag, and a new attribute-style @feature_flag(FLAG) decorator — all tenant-aware and all accepting either a FeatureFlagDefinition constant or the raw name, so callers never retype flag strings.
  • docs/module-authoring.md documents 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 check
  • make test-py — 753 passed (adds 30 new tests: registry + tenant overrides + consumer helpers + @feature_flag decorator incl. sync handler and missing-Request-param TypeError)
  • make test-js — 8 passed
  • Manual smoke: register a flag in a module, gate an endpoint with @feature_flag(FLAG), toggle from the admin UI, confirm per-tenant override wins over system override

https://claude.ai/code/session_01AKxpW8EjmzJHSfrsQWyoqn

claude added 7 commits April 20, 2026 08:45
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
@antosubash
antosubash merged commit d57a7ad into main Apr 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants