Feature Flags¶
A simple, configuration-based feature flags system for conditionally enabling or disabling functionality in the application.
Overview¶
The feature flags system allows you to:
Enable/disable features via environment variables or configuration
Protect routes with feature flag guards
Access feature flags in templates for conditional rendering
Programmatically check feature status in your code
Enable maintenance mode across the entire application
Configuration¶
Feature flags are configured in src/pydotorg/config.py using a Pydantic model:
class FeatureFlagsConfig(BaseModel):
"""Feature flags configuration for conditional functionality."""
enable_oauth: bool = True
enable_jobs: bool = True
enable_sponsors: bool = True
enable_search: bool = True
maintenance_mode: bool = False
Environment Variables¶
Override feature flags using environment variables with the FEATURES__ prefix:
# Disable OAuth
export FEATURES__ENABLE_OAUTH=false
# Enable maintenance mode
export FEATURES__MAINTENANCE_MODE=true
# Disable jobs
export FEATURES__ENABLE_JOBS=false
Or in your .env file:
FEATURES__ENABLE_OAUTH=false
FEATURES__MAINTENANCE_MODE=true
Usage¶
Route Guards¶
Protect routes so they are only accessible when a feature is enabled:
from litestar import Controller, get
from pydotorg.core.features import require_feature
class OAuthController(Controller):
path = "/oauth"
@get("/login", guards=[require_feature("enable_oauth")])
async def login(self) -> dict:
"""Only accessible when OAuth is enabled."""
return {"message": "OAuth login"}
When a feature is disabled or maintenance mode is active, the guard returns a 503 Service Unavailable response.
Dependency Injection¶
Inject feature flags into your handlers for programmatic checks:
from litestar import get
from pydotorg.core.features import FeatureFlags
@get("/status")
async def status(feature_flags: FeatureFlags) -> dict:
"""Check feature availability programmatically."""
response = {"available_features": []}
if feature_flags.is_enabled("enable_jobs"):
response["available_features"].append("jobs")
if feature_flags.is_enabled("enable_sponsors"):
response["available_features"].append("sponsors")
return response
Template Usage¶
Feature flags are automatically available in all Jinja templates:
{# Show OAuth login only when enabled #}
{% if feature_flags.enable_oauth %}
<div class="oauth-section">
<a href="/oauth/github">Login with GitHub</a>
<a href="/oauth/google">Login with Google</a>
</div>
{% endif %}
{# Show maintenance banner #}
{% if feature_flags.maintenance_mode %}
<div class="alert alert-warning">
<strong>Maintenance Mode:</strong> Some features are temporarily unavailable.
</div>
{% endif %}
{# Conditionally show jobs section #}
{% if feature_flags.enable_jobs %}
<div class="jobs-section">
<a href="/jobs">Browse Job Listings</a>
</div>
{% endif %}
Application State¶
Feature flags are also available on the application state:
from litestar import Request
@get("/check")
async def check(request: Request) -> dict:
"""Access feature flags from app state."""
flags = request.app.state.feature_flags
return {"maintenance_mode": flags.maintenance_mode}
Available Feature Flags¶
Flag |
Description |
Default |
|---|---|---|
|
OAuth authentication (GitHub, Google) |
|
|
Job listings functionality |
|
|
Sponsorship management and display |
|
|
Site-wide search functionality |
|
|
Application-wide maintenance mode |
|
Maintenance Mode¶
When maintenance_mode is enabled:
All feature flag guards block access with 503 responses
Templates can display maintenance banners
Critical endpoints (health checks, admin) remain accessible
# Enable maintenance mode
export FEATURES__MAINTENANCE_MODE=true
Adding New Feature Flags¶
To add a new feature flag:
1. Update Configuration¶
Add the field to FeatureFlagsConfig in config.py:
class FeatureFlagsConfig(BaseModel):
enable_oauth: bool = True
enable_new_feature: bool = False # Add this
2. Update FeatureFlags Class¶
Update FeatureFlags.__init__() in features.py:
def __init__(
self,
*,
enable_oauth: bool = True,
enable_new_feature: bool = False, # Add this
) -> None:
self.enable_oauth = enable_oauth
self.enable_new_feature = enable_new_feature # Add this
3. Update to_dict Method¶
Update FeatureFlags.to_dict() in features.py:
def to_dict(self) -> dict[str, bool]:
return {
"enable_oauth": self.enable_oauth,
"enable_new_feature": self.enable_new_feature, # Add this
}
4. Update Providers¶
Update the providers in dependencies.py and main.py to pass the new flag.
5. Use the Feature Flag¶
@get("/new-feature", guards=[require_feature("enable_new_feature")])
async def new_feature_endpoint() -> dict:
return {"message": "New feature"}
Testing Feature Flags¶
Feature flags can be easily mocked in tests:
from litestar import Litestar, get
from litestar.testing import TestClient
from pydotorg.core.features import FeatureFlags, require_feature
def test_disabled_feature():
@get("/test", guards=[require_feature("enable_oauth")])
def handler() -> dict:
return {"status": "ok"}
def init_app(app: Litestar) -> None:
app.state.feature_flags = FeatureFlags(enable_oauth=False)
app = Litestar(
route_handlers=[handler],
on_app_init=[init_app],
)
client = TestClient(app=app)
response = client.get("/test")
assert response.status_code == 503
def test_enabled_feature():
@get("/test", guards=[require_feature("enable_oauth")])
def handler() -> dict:
return {"status": "ok"}
def init_app(app: Litestar) -> None:
app.state.feature_flags = FeatureFlags(enable_oauth=True)
app = Litestar(
route_handlers=[handler],
on_app_init=[init_app],
)
client = TestClient(app=app)
response = client.get("/test")
assert response.status_code == 200
Best Practices¶
Use guards for route protection - Always use
require_feature()guards rather than manual checks in handlersKeep flags boolean - Feature flags should be simple on/off switches
Document flag purpose - Add clear docstrings explaining what each flag controls
Default to enabled - New features should default to
Trueunless explicitly riskyClean up old flags - Remove feature flags once features are stable and permanently enabled
Test both states - Always test your code with flags both enabled and disabled
Architecture¶
Core Components¶
src/pydotorg/core/features.py
- FeatureFlags class: Manages feature flag state
- require_feature() guard factory: Creates route guards
src/pydotorg/core/dependencies.py
- provide_feature_flags(): Dependency provider
- get_core_dependencies(): Aggregates core dependencies
src/pydotorg/config.py
- FeatureFlagsConfig: Pydantic model for configuration
- Integrated into main Settings class
src/pydotorg/main.py
- Template context processor: Adds flags to templates
- App initialization: Adds flags to app state
Data Flow¶
Environment Variables / .env
|
v
Settings (Pydantic validation)
|
v
FeatureFlags instance
|
+---> Route Guards (require_feature)
+---> Dependency Injection (handlers)
+---> Template Context (Jinja)
+---> App State (request.app.state)
Migration Guide¶
If you are migrating existing code to use feature flags:
Before¶
@get("/oauth/login")
async def oauth_login() -> dict:
if not settings.oauth_enabled:
raise ServiceUnavailableException("OAuth disabled")
return {"message": "OAuth login"}
After¶
@get("/oauth/login", guards=[require_feature("enable_oauth")])
async def oauth_login() -> dict:
return {"message": "OAuth login"}
The guard handles the feature check automatically and provides consistent error responses.