Feature Flags · Service

Phase 2a+ HTTP boundary · FastAPI scaffold · wraps evaluator.py (parity-locked with evaluator.js)

Phase 2a+
Live: HTTP contract · FastAPI scaffold · 4 endpoints · Python evaluator port (parity 19/19 with evaluator.js). Not live: JWT auth · Redis cache · pubsub invalidation · override stores · transition API · admin flip UI.
Dev-mode context is read from X-PTT-* headers or query params · bind to 127.0.0.1 only · not safe for public exposure.

Bring the service up locally

Requirement: Python ≥ 3.11 · port 8080 free (override with FF_PORT)
Start command: bash docs/runtime/feature-flags-service/run.sh
What it does: create .venv → install FastAPI + uvicorn → run verify_parity.py (must be 19/19) → start uvicorn on 127.0.0.1:8080.
Parity gate: if the Python port ever diverges from evaluator.js, the script aborts before binding.

Live tester

Point the URL bar at your running service and hit a flag. Nothing in this page stores state — context stays in the form.

Base URL

Endpoints

Dev-mode context

No JWT in this phase. Context comes from headers or query params. Headers win on conflict.

HeaderQuery fallbackMeaning
X-PTT-User-Id?user=Stable id · required · used for bucketing
X-PTT-Tenant-Id?tenant=Tenant scope · optional for platform flags
X-PTT-Tier?tier=RBAC tier · default anonymous
X-PTT-Env?env=prod / staging / dev · default dev
X-PTT-Role-Key?role_key=Platform vs tenant role · optional
X-Request-IdCorrelation id · echoed in response

Missing user_id → 400

The service returns HTTP 400 invalid_request with a hint field when no user id is supplied. This is deliberate — bucketing determinism breaks without a stable id.

Phase 2b plan (JWT)

Mechanism: Authorization: Bearer <jwt> · RS256 signed via IdP · JWKS rotation supported.
Claims required: sub (user_id) · tenant_id · tier · role_key · exp.
Header fallback: disabled when JWT middleware is active.
Deferred because: needs IdP integration + override store coupling (Phase 2b).

Non-goals this phase

Copy · paste · run

Assumes the service is at http://127.0.0.1:8080. Replace host / port as needed.

Health

curl -s http://127.0.0.1:8080/api/flags/health | jq .

Single eval · staff tier → internal stage

curl -s 'http://127.0.0.1:8080/api/flags/eval?key=flags.registry_v1&user=U-1&tier=staff&env=prod' | jq .data

Single eval · dep-gated flag → dep_unsatisfied

curl -s 'http://127.0.0.1:8080/api/flags/eval?key=cases.runtime_v1&user=U-2&tier=admin&env=prod&tenant=pty-zeroth' | jq .data

Single eval · header context

curl -s 'http://127.0.0.1:8080/api/flags/eval?key=flags.registry_v1' \
  -H 'X-PTT-User-Id: hdr-user' \
  -H 'X-PTT-Tier: admin' \
  -H 'X-PTT-Env: prod' | jq .data

Batch eval · shared context + per-item override

curl -s -X POST 'http://127.0.0.1:8080/api/flags/eval/batch' \
  -H 'Content-Type: application/json' \
  -d '{
    "context": {"user_id":"U-batch","tier":"staff","env":"prod","tenant_id":"pty-zeroth"},
    "flags": [
      "flags.registry_v1",
      "cases.runtime_v1",
      {"flag_key":"wizard.runtime_v1","tier":"platinum"}
    ]
  }' | jq .data

Registry snapshot · summary mode

curl -s 'http://127.0.0.1:8080/api/flags/registry?summary=true' | jq '.data | {count, flags: .flags[0:3]}'

Missing user_id → 400

curl -s -w 'HTTP=%{http_code}\n' 'http://127.0.0.1:8080/api/flags/eval?key=flags.registry_v1' | jq

http_contract.json

Source: docs/runtime/feature-flags-service/http_contract.json · Parity reference: ../feature-flags/evaluator_contract.json

loading…