Skip to main content
This quickstart uses a real FastAPI codebase:
  • https://github.com/fastapi/full-stack-fastapi-template
Goal:
  1. Scan live OpenAPI.
  2. Generate initial policy artifacts.
  3. Wire policy into route code and write decision logs.
  4. Switch from observe to enforce mode.

1) Boot the FastAPI template

git clone https://github.com/fastapi/full-stack-fastapi-template.git
cd full-stack-fastapi-template

# start only Postgres from compose
docker compose up -d db

# backend local env
cd backend
uv sync
source .venv/bin/activate
pip install kernite

# run migrations + initial data
bash scripts/prestart.sh

# start API in observe mode
KERNITE_MODE=observe fastapi dev app/main.py --host 127.0.0.1 --port 8000
Keep this terminal running. Use a second terminal for the steps below.

2) Pull OpenAPI and generate policy artifacts

From repo root (full-stack-fastapi-template):
mkdir -p backend/.kernite
curl -sS http://127.0.0.1:8000/api/v1/openapi.json -o backend/.kernite/openapi.json

uvx kernite policy generate \
  --schema backend/.kernite/openapi.json \
  --out-dir backend/.kernite
Generated files:
  • backend/.kernite/policy-bundle.generated.json
  • backend/.kernite/policy-map.generated.json
  • backend/.kernite/policy-generation-report.json
Coverage check (expected to fail strict initially because source OpenAPI has no x-kernite fields yet):
uvx kernite check \
  --schema backend/.kernite/openapi.json \
  --report-out backend/.kernite/kernite-check.strict.json || true

jq '.summary' backend/.kernite/kernite-check.strict.json

3) Add one demo deny rule to generated policy

This makes mode switching obvious for one real route: POST /api/v1/users/signup.
POLICY_KEY=$(jq -r '.operations[] | select(.path=="/api/v1/users/signup" and .method=="post") | .policy_key' backend/.kernite/policy-map.generated.json)

jq --arg policy_key "$POLICY_KEY" '
  (.policies[] | select(.policy_key == $policy_key) | .rules) |=
    (if any(.[]; .rule_key == "demo_password_allowlist") then
       .
     else
       . + [
         {
           "rule_key": "demo_password_allowlist",
           "rule_definition": {
             "type": "allowed_values",
             "field": "password",
             "values": ["demo-pass-123"]
           },
           "reason_code": "user_signup_password_not_allowlisted",
           "reason_message": "Demo guard: password must equal demo-pass-123."
         }
       ]
     end)
' backend/.kernite/policy-bundle.generated.json > backend/.kernite/policy-bundle.generated.next.json

mv backend/.kernite/policy-bundle.generated.next.json backend/.kernite/policy-bundle.generated.json

4) Wire policy into route code (observe mode)

Create backend/app/core/kernite_guard.py:
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

from kernite import JsonlDecisionSink, evaluate_execute_controlled

POLICY_DIR = Path(__file__).resolve().parents[2] / ".kernite"
BUNDLE = json.loads((POLICY_DIR / "policy-bundle.generated.json").read_text(encoding="utf-8"))
MAPPING = json.loads((POLICY_DIR / "policy-map.generated.json").read_text(encoding="utf-8"))

POLICIES = {policy["policy_key"]: policy for policy in BUNDLE.get("policies", [])}
OPERATIONS = {
    (item["method"].upper(), item["path"]): item
    for item in MAPPING.get("operations", [])
}
SINK = JsonlDecisionSink(POLICY_DIR / "decision-events.jsonl")


def evaluate_route(
    *,
    method: str,
    path: str,
    principal_id: str,
    payload: dict[str, Any],
) -> dict[str, Any]:
    operation = OPERATIONS.get((method.upper(), path))
    if operation is None:
        return {
            "allow_write": True,
            "mode": "skip",
            "decision_raw": "skipped",
            "decision_effective": "approved",
            "governance": None,
            "sink_status": "skipped",
        }

    policy_key = operation.get("policy_key")
    selected_policies = [POLICIES[policy_key]] if policy_key in POLICIES else []

    request_body = {
        "workspace_id": "workspace-demo",
        "principal": {"type": "token", "id": principal_id},
        "object_type": operation["object_type"],
        "operation": operation["operation"],
        "payload": payload,
        "policy_context": {
            "governed": bool(operation.get("governed", False)),
            "selected_policies": selected_policies,
            "governed_scopes": BUNDLE.get("governed_scopes", []),
            "policy_selection_reason_code": "policy_selected_workspace_default",
        },
    }

    return evaluate_execute_controlled(
        request_body,
        mode=os.getenv("KERNITE_MODE", "observe"),
        sink=SINK,
        sink_failure_policy="fail_open",
    )
Patch backend/app/api/routes/users.py inside register_user(...):
from app.core.kernite_guard import evaluate_route

# inside register_user(...)
governance = evaluate_route(
    method="POST",
    path="/api/v1/users/signup",
    principal_id=f"public:{user_in.email}",
    payload=user_in.model_dump(exclude_none=True),
)
if not governance["allow_write"]:
    raise HTTPException(
        status_code=403,
        detail={
            "reason_codes": governance["governance"]["data"]["reason_codes"],
            "trace_hash": governance["governance"]["data"]["trace_hash"],
        },
    )
The backend dev server reloads automatically after file changes.

5) Observe mode demo

Run a signup request that should be denied by policy but still allowed in observe mode:
curl -sS -X POST http://127.0.0.1:8000/api/v1/users/signup \
  -H 'content-type: application/json' \
  -d '{"email":"kernite-observe-1@example.com","password":"not-allowed","full_name":"Observe Demo"}' | jq .
Inspect decision event:
tail -n 1 backend/.kernite/decision-events.jsonl | jq '{mode, decision_raw, decision_effective, allow_write, reason_codes, sink_status}'
Expected:
  • mode = "observe"
  • decision_raw = "denied"
  • decision_effective = "approved"
  • allow_write = true

6) Enforce mode demo

Restart backend in enforce mode:
cd backend
source .venv/bin/activate
KERNITE_MODE=enforce fastapi dev app/main.py --host 127.0.0.1 --port 8000
Denied request (now blocked):
curl -sS -X POST http://127.0.0.1:8000/api/v1/users/signup \
  -H 'content-type: application/json' \
  -d '{"email":"kernite-enforce-deny-1@example.com","password":"not-allowed","full_name":"Enforce Deny"}' | jq .
Approved request:
curl -sS -X POST http://127.0.0.1:8000/api/v1/users/signup \
  -H 'content-type: application/json' \
  -d '{"email":"kernite-enforce-ok-1@example.com","password":"demo-pass-123","full_name":"Enforce OK"}' | jq .
You now have one complete real-project flow:
  • OpenAPI scan
  • initial policy generation
  • in-code decision logging
  • observe to enforce switch with the same integration contract