Skip to content

Feature Flags

Feature flags (also called feature toggles) let you change your application's behavior per user without redeploying. The system is built on the OpenFeature standard and supports boolean, string, integer, float, and JSON flags, multi-condition targeting rules, user segments, percentage rollouts, and prerequisites.

Optional dependency

Feature flags require the flags extra:

uv add "api-shield[flags]"
# or: pip install "api-shield[flags]"


Overview

A feature flag has:

  • Variations: the possible values it can return (on/off, "dark"/"light", 10/50, etc.)
  • Targeting: rules that decide which variation a specific user receives
  • Fallthrough: the default variation when no rule matches (a fixed value or a percentage rollout)
  • Kill-switch: enabled=False skips all rules and returns the off_variation immediately

Evaluation always follows this order:

1. Flag disabled?           → off_variation
2. Prerequisite flags?      → off_variation if any prereq fails
3. Individual targets?      → fixed variation for specific user keys
4. Targeting rules?         → first matching rule wins
5. Fallthrough              → fixed variation or percentage bucket

Installation and setup

uv add "api-shield[flags]"

Call engine.use_openfeature() once before your first evaluation, then access the flag client through engine.flag_client:

from shield.core.config import make_engine

engine = make_engine()
engine.use_openfeature()   # activates the feature flag subsystem

The flag client is a standard OpenFeature client — any OpenFeature-aware code works with it directly.


Your first flag

from shield.core.feature_flags.models import (
    FeatureFlag, FlagType, FlagVariation, RolloutVariation,
    EvaluationContext,
)

# 1. Define and save the flag
await engine.save_flag(
    FeatureFlag(
        key="new-checkout",
        name="New Checkout Flow",
        type=FlagType.BOOLEAN,
        variations=[
            FlagVariation(name="on",  value=True),
            FlagVariation(name="off", value=False),
        ],
        off_variation="off",
        fallthrough=[                       # 20% of users get "on"
            RolloutVariation(variation="on",  weight=20_000),
            RolloutVariation(variation="off", weight=80_000),
        ],
    )
)

# 2. Evaluate it in a route handler
ctx = EvaluationContext(key=user_id)
enabled = await engine.flag_client.get_boolean_value("new-checkout", False, ctx)

Rollout weights are integers out of 100_000. The above gives exactly 20% to "on" and 80% to "off". Bucketing is deterministic: the same user_id always lands in the same bucket.

Seeding flags at startup

When pre-loading flags in a lifespan or startup function, pass audit=False so these programmatic writes do not appear in the audit log:

@asynccontextmanager
async def lifespan(_):
    await engine.save_flag(FeatureFlag(key="new-checkout", ...), audit=False)
    yield
Flags created or updated through the dashboard, REST API, or CLI always audit regardless of this parameter.


Flag types

Type Method Python type
FlagType.BOOLEAN get_boolean_value bool
FlagType.STRING get_string_value str
FlagType.INTEGER get_integer_value int
FlagType.FLOAT get_float_value float
FlagType.JSON get_object_value dict / list

All evaluation methods share the same signature: (flag_key, default_value, context).

# String flag
theme = await engine.flag_client.get_string_value("ui-theme", "light", ctx)

# Integer flag
page_size = await engine.flag_client.get_integer_value("page-size", 10, ctx)

# Float flag
discount = await engine.flag_client.get_float_value("discount-rate", 0.0, ctx)

# JSON flag — returns a dict
config = await engine.flag_client.get_object_value("feature-config", {}, ctx)

Evaluation context

EvaluationContext identifies who is making the request. The key field is required; use a stable user or session identifier. Everything else is optional:

ctx = EvaluationContext(
    key=user.id,           # required — used for individual targeting + rollout bucketing
    kind="user",           # optional — defaults to "user"
    email=user.email,      # accessible in rules as the "email" attribute
    ip=request.client.host,
    country=user.country,
    app_version="2.3.1",
    attributes={           # any extra attributes your rules need
        "plan": user.plan,
        "role": user.role,
    },
)

Named fields (email, ip, country, app_version) are accessible in targeting rules by the same names. Custom attributes go in attributes.


Targeting rules

Targeting rules serve a specific variation to users who match certain conditions.

Attribute-based rule

from shield.core.feature_flags.models import TargetingRule, RuleClause, Operator

FeatureFlag(
    key="ui-theme",
    ...
    rules=[
        TargetingRule(
            description="Corporate users → dark theme",
            clauses=[
                RuleClause(
                    attribute="email",
                    operator=Operator.ENDS_WITH,
                    values=["@acme.com"],
                )
            ],
            variation="dark",
        )
    ],
)

Multiple clauses (AND logic)

All clauses within a rule must match (AND). Multiple values within one clause are OR-ed.

TargetingRule(
    description="GB Pro users → full discount",
    clauses=[
        RuleClause(attribute="country", operator=Operator.IS,  values=["GB"]),
        RuleClause(attribute="plan",    operator=Operator.IN,  values=["pro", "enterprise"]),
    ],
    variation="full",
)

Negation

Flip the result of any clause with negate=True:

RuleClause(attribute="plan", operator=Operator.IS, values=["free"], negate=True)
# matches any user NOT on the free plan

Available operators

Category Operators
Equality IS, IS_NOT
String CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH, MATCHES, NOT_MATCHES
Numeric GT, GTE, LT, LTE
Date BEFORE, AFTER (ISO-8601 string comparison)
Collection IN, NOT_IN
Segment IN_SEGMENT, NOT_IN_SEGMENT
Semver SEMVER_EQ, SEMVER_LT, SEMVER_GT

Individual targeting

Override rules for specific users by listing their context keys in targets. Individual targets are evaluated after prerequisites but before rules, and always win.

FeatureFlag(
    key="new-checkout",
    ...
    targets={
        "on": ["beta_tester_1", "beta_tester_2"],   # these users always get "on"
        "off": ["opted_out_user"],                   # this user always gets "off"
    },
)

Segments

A segment is a named, reusable group of users. Define it once and reference it in any flag's targeting rules with Operator.IN_SEGMENT.

Creating a segment

from shield.core.feature_flags.models import Segment, SegmentRule, RuleClause, Operator

# Explicit include list
await engine.save_segment(Segment(
    key="beta-users",
    name="Beta Users",
    included=["user_123", "user_456", "user_789"],
))

# Attribute-based rules (any matching rule → user is in the segment)
await engine.save_segment(Segment(
    key="enterprise-plan",
    name="Enterprise Plan",
    rules=[
        SegmentRule(clauses=[
            RuleClause(attribute="plan", operator=Operator.IS, values=["enterprise"]),
        ]),
    ],
))

# Exclude specific users even if they match a rule
await engine.save_segment(Segment(
    key="paid-users",
    name="Paid Users",
    rules=[
        SegmentRule(clauses=[
            RuleClause(attribute="plan", operator=Operator.IN, values=["pro", "enterprise"]),
        ]),
    ],
    excluded=["test_account", "demo_user"],  # always excluded, overrides rules
))

Pass audit=False when seeding segments at startup, same as with flags.

Segment evaluation order

For a given context key k:

  1. k in excludednot in segment
  2. k in included → in segment
  3. Any rules entry matches → in segment
  4. Otherwise → not in segment

Segment key ≠ user key

The segment key (e.g. "beta-users") is the segment's identifier. To make a user with user_id="alice" part of this segment, add "alice" to included — or add a segment rule that matches her attributes. Simply naming the segment "alice" does not put her in it.

Using a segment in a flag rule

TargetingRule(
    description="Beta users get the new flow",
    clauses=[
        RuleClause(
            attribute="key",          # evaluates ctx.key against the segment
            operator=Operator.IN_SEGMENT,
            values=["beta-users"],    # segment key to reference
        )
    ],
    variation="on",
)

Managing segments from the dashboard

Open the Segments page (/shield/segments) and click a segment key or Edit to:

  • Add or remove users from the Included and Excluded lists
  • Add targeting rules — attribute-based conditions evaluated when a user isn't in the explicit lists

Managing segments from the CLI

# List all segments
shield segments list

# Inspect a segment
shield segments get beta-users

# Create a segment
shield segments create beta_users --name "Beta Users"

# Add users to the included list
shield segments include beta_users --context-key user_123,user_456

# Remove users via the excluded list
shield segments exclude beta_users --context-key opted_out_user

# Add an attribute-based targeting rule
shield segments add-rule beta_users --attribute plan --operator in --values pro,enterprise
shield segments add-rule beta_users --attribute country --operator is --values GB --description "UK users"

# Remove a rule (use 'shield segments get' to find rule IDs)
shield segments remove-rule beta_users --rule-id <uuid>

# Delete a segment
shield segments delete beta_users

Prerequisites

Prerequisites let a flag depend on another flag. The dependent flag only proceeds to its rules if the prerequisite flag evaluates to a specific variation.

from shield.core.feature_flags.models import Prerequisite

FeatureFlag(
    key="advanced-dashboard",
    ...
    prerequisites=[
        Prerequisite(flag_key="auth-v2", variation="enabled"),
        # advanced-dashboard only evaluates if auth-v2 → "enabled"
    ],
)

Prerequisites are recursive up to a depth of 10. Circular dependencies are prevented at write time.


Sync evaluation (plain def handlers)

FastAPI runs plain def route handlers in a thread pool. Use engine.sync.flag_client for thread-safe synchronous evaluation without any event loop bridging:

@router.get("/dashboard")
def dashboard(request: Request, user_id: str = "anonymous"):
    enabled = engine.sync.flag_client.get_boolean_value(
        "new-dashboard", False, {"targeting_key": user_id}
    )
    return {"new_dashboard": enabled}

Admin dashboard

Flags page (/shield/flags)

Lists all flags with key, type, status, variations, and fallthrough. Use the search box and type/status filters to narrow the list. Click a flag key to open the detail page.

Flag detail page

Tab Contents
Overview Key metrics: evaluation count, rule match rate, fallthrough rate, top variations
Targeting Add / remove prerequisite flags; manage individual targets; add / edit / delete targeting rules
Variations Add, rename, or remove variations; change the fallthrough and off-variation
Settings Edit name, description, tags, maintainer, temporary flag flag, and scheduled changes

Segments page (/shield/segments)

Lists all segments with included/excluded/rules counts. Click a segment to open its detail modal, or use the Edit button to manage included, excluded, and targeting rules.


CLI reference

shield flags

shield flags list                              # all flags
shield flags get new-checkout                  # flag detail
shield flags create new-checkout boolean       # create (interactive prompts follow)
shield flags enable new-checkout               # enable (kill-switch off)
shield flags disable new-checkout              # disable (kill-switch on)
shield flags delete new-checkout               # permanently delete

shield flags eval new-checkout --user user_123  # evaluate for a user

shield flags targeting new-checkout            # show targeting rules
shield flags add-rule new-checkout \
    --variation on \
    --segment beta-users                       # add segment-based rule
shield flags add-rule new-checkout \
    --variation on \
    --attribute plan --operator in --values pro,enterprise
shield flags remove-rule new-checkout --rule-id <uuid>

shield flags add-prereq new-checkout --flag auth-v2 --variation enabled
shield flags remove-prereq new-checkout --flag auth-v2

shield flags target new-checkout --variation on --context-key user_123
shield flags untarget new-checkout --context-key user_123

shield flags variations new-checkout          # list variations
shield flags edit new-checkout                # open interactive editor

shield segments

shield segments list
shield segments get beta-users
shield segments create beta_users --name "Beta Users"
shield segments include beta_users --context-key user_123,user_456
shield segments exclude beta_users --context-key opted_out
shield segments add-rule beta_users --attribute plan --operator in --values pro,enterprise
shield segments remove-rule beta_users --rule-id <uuid>
shield segments delete beta_users

Full example

Full example at examples/fastapi/feature_flags.py, covering all five flag types, individual targeting, attribute-based rules, percentage rollouts, and sync and async evaluation.

Run it with:

uv run uvicorn examples.fastapi.feature_flags:app --reload

Then visit:

  • http://localhost:8000/docs — Swagger UI
  • http://localhost:8000/shield/flags — flag management dashboard
  • http://localhost:8000/checkout?user_id=beta_tester_1 — targeted user (always "on")
  • http://localhost:8000/checkout?user_id=anyone_else — 20% rollout

Next step

Reference: Feature Flags →