Skip to main content
API Shield

Feature flags and runtime control for Python APIs — rollouts, rate limits, manage maintenance windows across single ASGI services or a multi-service fleet without redeploying.

PyPI Python versions License


[!IMPORTANT] api-shield is deprecated. This package has been renamed to waygate. No further releases will be published here.

Migrate now:

pip install waygate
# or: uv add waygate

Imports change from shield → waygate and the CLI command from shield → waygate. See the waygate repository for full documentation.


Key features

Core (shield.core)

These features are framework-agnostic and available to any adapter.

Feature Description
⚡ Zero-restart control State changes take effect immediately — no redeployment or server restart needed
🔄 Sync & async Full support for both async def and plain def route handlers — use await engine.* or engine.sync.*
🛡️ Fail-open by default If the backend is unreachable, requests pass through. Shield never takes down your API
🔌 Pluggable backends In-memory (default), file-based JSON, or Redis for multi-instance deployments
🖥️ Admin dashboard HTMX-powered UI with live SSE updates — no JS framework required
🖱️ REST API + CLI Full programmatic control from the terminal or CI pipelines — works over HTTPS remotely
📋 Audit log Every state change is recorded: who, when, what route, old status → new status
⏰ Scheduled windows asyncio-native scheduler — maintenance windows activate and deactivate automatically
🔔 Webhooks Fire HTTP POST on every state change — built-in Slack formatter and custom formatters supported
🚦 Rate limiting Per-IP, per-user, per-API-key, or global counters — tiered limits, burst allowance, runtime mutation
🚩 Feature flags Boolean, string, integer, float, and JSON flags — targeting rules, user segments, percentage rollouts, prerequisites, and a live evaluation stream. Built on the OpenFeature standard
🏗️ Shield Server Centralised control plane for multi-service architectures — SDK clients sync state via SSE with zero per-request latency
🌐 Multi-service CLI SHIELD_SERVICE env var scopes every command; shield services lists connected services

Framework adapters

FastAPI (shield.fastapi) — ✅ supported

Feature Description
🎨 Decorator-first DX @maintenance, @disabled, @env_only, @force_active, @deprecated, @rate_limit — state lives next to the route
📄 OpenAPI integration Disabled / env-gated routes hidden from /docs; deprecated routes flagged; live maintenance banners in the Swagger UI
🧩 Dependency injection All decorators work as Depends() — enforce shield state per-handler without middleware
🎨 Custom responses Return HTML, redirects, or any response shape for blocked routes — per-route or app-wide default on the middleware
🔀 ShieldRouter Drop-in APIRouter replacement that auto-registers route metadata with the engine at startup
API Shield OpenAPI integration API Shield maintenance banner in Swagger UI

Disabled and env-gated routes hidden from /docs. Maintenance banners injected live.


Install

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

Quickstart

FastAPI is the currently supported adapter. Litestar, Starlette, Quart, and Django (ASGI) are on the roadmap.

from fastapi import FastAPI
from shield.core.config import make_engine
from shield.fastapi import (
    ShieldMiddleware, ShieldAdmin, apply_shield_to_openapi,
    maintenance, env_only, disabled, force_active, deprecated,
)

engine = make_engine()

app = FastAPI()
app.add_middleware(ShieldMiddleware, engine=engine)

@app.get("/payments")
@maintenance(reason="DB migration — back at 04:00 UTC")
async def get_payments():
    return {"payments": []}

@app.get("/health")
@force_active
async def health():
    return {"status": "ok"}

apply_shield_to_openapi(app, engine)
app.mount("/shield", ShieldAdmin(engine=engine, auth=("admin", "secret")))
GET /payments  → 503  {"error": {"code": "MAINTENANCE_MODE", ...}}
GET /health    → 200  always

Manage routes from the CLI — no code changes, no restarts:

shield config set-url http://localhost:8000/shield
shield login admin
shield status
shield enable GET:/payments
shield global enable --reason "Deploying v2" --exempt /health
API Shield admin dashboard

Admin dashboard — route states, audit log, rate limits, and feature flags. No JS framework required.

Decorators

Decorator Effect Status
@maintenance(reason, start, end) Temporarily unavailable 503
@disabled(reason) Permanently off 503
@env_only("dev", "staging") Restricted to named environments 404 elsewhere
@deprecated(sunset, use_instead) Still works, injects deprecation headers 200
@force_active Bypasses all shield checks Always 200
@rate_limit("100/minute") Cap requests per IP, user, API key, or globally 429

Custom responses (FastAPI)

By default, blocked routes return a structured JSON error body. You can replace it with anything — HTML, a redirect, plain text, or your own JSON — in two ways:

Per-route — pass response= directly on the decorator:

from starlette.requests import Request
from starlette.responses import HTMLResponse, RedirectResponse
from shield.fastapi import maintenance, disabled

def maintenance_page(request: Request, exc: Exception) -> HTMLResponse:
    return HTMLResponse(
        f"<h1>Down for maintenance</h1><p>{exc.reason}</p>", status_code=503
    )

@router.get("/payments")
@maintenance(reason="DB migration", response=maintenance_page)
async def payments():
    return {"payments": []}

@router.get("/orders")
@maintenance(reason="Upgrade in progress", response=lambda *_: RedirectResponse("/status"))
async def orders():
    return {"orders": []}

Global default — set once on ShieldMiddleware, applies to every route without a per-route factory:

app.add_middleware(
    ShieldMiddleware,
    engine=engine,
    responses={
        "maintenance": maintenance_page,   # all maintenance routes
        "disabled": lambda req, exc: HTMLResponse(
            f"<h1>Gone</h1><p>{exc.reason}</p>", status_code=503
        ),
    },
)

Resolution order: per-route response= → global responses[...] → built-in JSON. The factory can be sync or async and receives the live Request and the ShieldException that triggered the block.

Rate limiting

from shield.fastapi.decorators import rate_limit

@router.get("/public/posts")
@rate_limit("10/minute")               # 10 req/min per IP
async def list_posts():
    return {"posts": [...]}

@router.get("/users/me")
@rate_limit("100/minute", key="user")  # per authenticated user
async def get_current_user():
    ...

@router.get("/reports")
@rate_limit(                           # tiered limits
    {"free": "10/minute", "pro": "100/minute", "enterprise": "unlimited"},
    key="user",
)
async def get_reports():
    ...

Policies can be mutated at runtime without redeploying (shield rl and shield rate-limits are aliases):

shield rl set GET:/public/posts 20/minute   # raise the limit live
shield rl reset GET:/public/posts           # clear counters
shield rl hits                              # blocked requests log

Requires api-shield[rate-limit]. Powered by limits.


Feature flags

api-shield ships a full feature flag system built on the OpenFeature standard. All five flag types, multi-condition targeting rules, user segments, percentage rollouts, and a live evaluation stream — managed from the dashboard or CLI with no code changes.

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

engine.use_openfeature()

# Define a boolean flag with a 20% rollout and individual targeting
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=[
            RolloutVariation(variation="on",  weight=20_000),  # 20%
            RolloutVariation(variation="off", weight=80_000),  # 80%
        ],
        targets={"on": ["beta_tester_1"]},   # individual targeting
        rules=[
            TargetingRule(
                description="Enterprise users always get the new flow",
                clauses=[RuleClause(attribute="plan", operator=Operator.IS, values=["enterprise"])],
                variation="on",
            )
        ],
    )
)

# Evaluate in an async route handler
ctx = EvaluationContext(key=user_id, attributes={"plan": user.plan})
enabled = await engine.flag_client.get_boolean_value("new-checkout", False, ctx)

# Evaluate in a sync def handler (thread-safe)
enabled = engine.sync.flag_client.get_boolean_value("new-checkout", False, {"targeting_key": user_id})

Manage flags and segments from the CLI:

shield flags list
shield flags eval new-checkout --user user_123
shield flags disable new-checkout          # kill-switch
shield flags enable new-checkout
shield flags stream                        # live evaluation events

shield segments create beta_users --name "Beta Users"
shield segments include beta_users --context-key user_123,user_456
shield segments add-rule beta_users --attribute plan --operator in --values pro,enterprise

Requires api-shield[flags].


Framework support

api-shield is built on the ASGI standard. The core (shield.core) is completely framework-agnostic and has zero framework imports. Any ASGI framework can be supported — either via a Starlette BaseHTTPMiddleware (for Starlette-based frameworks) or a raw ASGI callable for frameworks like Quart and Django that implement the ASGI spec independently.

ASGI frameworks

Framework Status Adapter
FastAPI ✅ Supported shield.fastapi
Litestar 🔜 Planned —
Starlette 🔜 Planned —
Quart 🔜 Planned —
Django (ASGI) 🔜 Planned —

Want support for another ASGI framework? Open an issue.

WSGI frameworks (Flask, Django, …)

[!IMPORTANT] WSGI support is out of scope for this project.

api-shield is an ASGI-native library. Bolting WSGI support in through shims or patches would require a persistent background event loop, thread-bridging hacks, and a fundamentally different middleware model — complexity that would compromise the quality and reliability of both layers.

WSGI framework support (Flask, Django, Bottle, …) will be delivered as a separate, dedicated project designed from the ground up for the synchronous request model. This keeps both projects clean, well-tested, and maintainable without trade-offs.

Watch this repo or open an issue to be notified when the WSGI companion project launches.


Backends

Embedded mode (single service)

Backend Persistence Multi-instance Best for
MemoryBackend No No Development, tests
FileBackend Yes No (single process) Simple single-instance prod
RedisBackend Yes Yes Load-balanced / multi-worker prod

For rate limiting in multi-worker deployments, use RedisBackend — counters are atomic and shared across all processes.

Shield Server mode (multi-service)

Run a dedicated ShieldServer process and connect each service via ShieldSDK. State is managed centrally; enforcement happens locally with zero per-request network overhead.

# Shield Server (centralised — runs once)
from shield.server import ShieldServer
shield_app = ShieldServer(backend=MemoryBackend(), auth=("admin", "secret"))

# Each service (connects to the Shield Server)
from shield.sdk import ShieldSDK
sdk = ShieldSDK(server_url="http://shield-server:9000", app_id="payments-service")
sdk.attach(app)
Scenario Shield Server backend SDK rate_limit_backend
Multi-service, single replica each MemoryBackend or FileBackend not needed
Multi-service, multiple replicas RedisBackend RedisBackend (shared counters)

Documentation

Full documentation at attakay78.github.io/api-shield

Tutorial Get started in 5 minutes
Decorators reference All decorator options
Rate limiting Per-IP, per-user, tiered limits
Feature flags Targeting rules, segments, rollouts, live events
ShieldEngine reference Programmatic control
Backends Memory, File, Redis, Shield Server, custom
Admin dashboard Mounting ShieldAdmin
CLI reference All CLI commands
Shield Server guide Multi-service centralized control
Distributed deployments Multi-instance backend guide
Production guide Monitoring & deployment automation

License

MIT

Metadata

Release files for api-shield 0.8.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for api-shield 0.8.3
File Size Uploaded
api_shield-0.8.3.tar.gz 1.3 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for api-shield 0.8.3
File Interpreter ABI Platform
api_shield-0.8.3-py3-none-any.whl Python 3 none any Details

Total release size: 1.6 MB

Release files / api_shield-0.8.3.tar.gz

Download URL api_shield-0.8.3.tar.gz
Size 1.3 MB
Tags Source
SHA-256 checksum
How to use checksums
0cb81f426ba37a82d9ff60ae3e56248b8a2f28f873d53ed527ac68a4ac6c8d03
BLAKE2b-256 checksum
How to use checksums
19f85ad0fa61059d3856607cb252e7c161d3f28e6d6fb1ce30b981a224aba30e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Release files / api_shield-0.8.3-py3-none-any.whl

Download URL api_shield-0.8.3-py3-none-any.whl
Size 297.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c6c75968b3a34e4108e9551e55e7dda310bf3fe21e96dcac697ba9cd583a95d0
BLAKE2b-256 checksum
How to use checksums
6f66ce6b303be63532692cd9b4db9c5706bb3b187a0b629b617b5f2920756908
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Release history Release notifications | RSS feed

This release

0.8.3 This release

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page