Skip to content

Feature flags

Turn features on and off without a redeploy: gradual rollouts, kill-switches, beta behind a flag. The SDK ships a FeatureFlags service over pluggable backends (static env, runtime Redis, or both layered) plus a FastAPI dependency that gates routes.

Quick start

from tempest_fastapi_sdk import FeatureFlags, MemoryFeatureFlagBackend

flags = FeatureFlags(MemoryFeatureFlagBackend({"new_checkout": True}))

if await flags.is_enabled("new_checkout"):
    ...                                                       # new path

is_enabled(name) returns the flag value, or the service default (False) when the flag is unset. Pass default=True per call to flip that locally. Toggle with enable / disable / set, and list everything with all().

Backends

Backend When to use
MemoryFeatureFlagBackend(initial=...) Tests and dev (in-process).
EnvFeatureFlagBackend(prefix="FEATURE_") Static config — new_checkout reads FEATURE_NEW_CHECKOUT. Read-only (set raises).
RedisFeatureFlagBackend(redis_client, key="feature_flags") Runtime toggling, shared across replicas (one Redis hash).
CompositeFeatureFlagBackend([redis, env]) Layered: the Redis override beats the env default.

Values read as truthy

1, true, yes, on, t, y (case-insensitive) become True; anything else is False. Applies to both env and Redis.

Production: Redis over env

The recommended pattern uses env as the static default (shipped with the deploy) and Redis as the runtime override (the team toggles without cutting a release):

from redis.asyncio import Redis

from tempest_fastapi_sdk import (
    CompositeFeatureFlagBackend,
    EnvFeatureFlagBackend,
    FeatureFlags,
    RedisFeatureFlagBackend,
)


def build_flags(redis: Redis) -> FeatureFlags:
    """Build the flags service with a Redis override over an env default.

    Args:
        redis (Redis): A connected async Redis client.

    Returns:
        FeatureFlags: The service ready to inject.
    """
    backend = CompositeFeatureFlagBackend(
        [
            RedisFeatureFlagBackend(redis, key="feature_flags"),  # runtime
            EnvFeatureFlagBackend(prefix="FEATURE_"),             # default
        ]
    )
    return FeatureFlags(backend)

Gating a route

make_flag_dependency(flags, name) returns an async dependency that lets the route through only when the flag is on. Otherwise it raises an AppException in the SDK envelope — 404 by default, so the feature simply "doesn't exist":

from fastapi import APIRouter, Depends

from tempest_fastapi_sdk import make_flag_dependency

from src.api.dependencies.resources import get_flags

router = APIRouter()
flags = get_flags()


@router.get(
    "/checkout/v2",
    dependencies=[Depends(make_flag_dependency(flags, "new_checkout"))],
)
async def checkout_v2() -> dict[str, bool]:
    """Only answers while ``new_checkout`` is on."""
    return {"ok": True}

For a kill-switch on something legacy, invert the gate with enabled=False (the route answers only while the flag is off):

@router.get(
    "/legacy",
    dependencies=[
        Depends(make_flag_dependency(flags, "legacy_disabled", enabled=False)),
    ],
)
async def legacy() -> dict[str, bool]:
    """Stops answering the moment ``legacy_disabled`` is turned on."""
    return {"ok": True}

status_code, detail and code are configurable — use status_code=403 to signal "exists but forbidden" instead of hiding it with 404.

Recap

  • FeatureFlags(backend, default=False)is_enabled / enable / disable / set / all.
  • Backends: Memory (dev), Env (static read-only), Redis (runtime), Composite (layered).
  • make_flag_dependency(flags, name, enabled=True, status_code=404) gates routes.
  • Redis override over an env default is the production pattern — toggle without a redeploy.