Skip to main content

Persistence (Governance Evidence Sinks)

Kernite is dependency-free and intentionally stateless. It does not persist logs or write to your database. In production, persist governance evidence in your application layer (or an adjacent service). Capture the decision contract from evaluate_execute(...) or POST /execute and write it to a durable sink (DB, object storage, log pipeline, etc.).

Why persist governance evidence

Persisting governance evidence enables:
  • Auditability: reproduce what was evaluated and why for any write attempt.
  • Security posture reporting: quantify what was blocked (deny events) and why.
  • Operational debugging: reason codes and trace hashes reduce time-to-resolution.
  • Analytics: top deny reasons, noisy principals, hot operations, and policy regressions.

Persist both approved and denied decisions

Do not store only approvals. Store:
  • approved decisions (writes that were allowed)
  • denied decisions (writes that were blocked)
  • application outcomes for request errors (for example invalid request or transport failure)
Most security value comes from denied events, meaning what your guard actually prevented.

The decision contract (what to store)

Kernite decision values are approved or denied. If you track error, treat it as an application-level outcome, not a Kernite decision enum. Store these fields in your sink:
  • created_at (server timestamp)
  • workspace_id
  • principal_id
  • object_type
  • operation
  • decision (approved | denied)
  • outcome (approved | denied | error) optional, app-level
  • reason_codes[]
  • ctx_id
  • trace_hash
  • idempotency_key
  • policy_version (if available)
  • latency_ms (if measured)
Optionally store:
  • request_json (redacted/sanitized)
  • response_json (full decision response; may be large)
Tip: if you only store one fat blob, store full response_json. If you need analytics, store indexable columns plus optional JSON.

Transaction semantics: where persistence should happen

A robust write path is:
  1. Evaluate: evaluate_execute(...) (or POST /execute)
  2. Persist governance evidence (approved/denied/error outcome)
  3. If decision == "approved", perform the actual mutation
  4. Return {ok, data, governance}

Strong pattern: same transaction for evidence and mutation

If your app uses a DB transaction, write both governance evidence and domain mutation in the same transaction. This prevents missing evidence for successful writes.

Failure policy: fail-closed vs fail-open

Decide what happens if your sink is unavailable.

Fail-closed (strict audit/compliance)

If sink write fails, deny the operation. This guarantees evidence exists for any allowed mutation.

Fail-open (availability first) + outbox

If availability matters more, allow mutation even if sink write fails, then retry evidence persistence via outbox table or queue.

Patterns by sink type

Pros:
  • easy querying (top deny reasons, per-principal rates, per-operation breakdown)
  • optional joins with domain data
Cons:
  • schema and retention planning required
Suggested schema:
  • index workspace_id, principal_id, decision, created_at
  • unique key (workspace_id, idempotency_key) for retry dedupe

Object storage sink (S3/GCS/Azure Blob)

Pros:
  • cheap and durable
  • good for long-term retention
Cons:
  • querying needs secondary system (Athena/BigQuery/etc.) or ETL
Recommended format:
  • JSONL partitioned by date, for example s3://bucket/kernite/year=YYYY/month=MM/day=DD/*.jsonl

Log pipeline sink (OpenTelemetry/Datadog/Cloud Logging)

Pros:
  • immediate dashboards and alerts
  • easy anomaly detection (deny spikes)
Cons:
  • retention and full-fidelity JSON may be expensive
Tip:
  • emit metric counters for decision and reason_codes
  • emit structured logs including trace_hash and ctx_id

Redaction and sensitive data

Governance evidence may contain sensitive data (PII, secrets, tokens). If you store request_json or full response_json, apply redaction.
  • remove or hash emails, phone numbers, addresses
  • remove tokens, credentials, and secrets
  • prefer IDs/references over raw payload values
Safe default:
  • store indexable fields plus reason_codes
  • store a redacted subset of request/response JSON

Example: minimal persistence in an application write path

Replace persist_evidence(...) with your sink implementation.
from kernite import evaluate_execute

def guard_and_write(mutate_fn, req: dict, idempotency_key: str):
    result = evaluate_execute(req, idempotency_key=idempotency_key)
    d = result["data"]

    persist_evidence(
        workspace_id=req["workspace_id"],
        principal_id=req["principal"]["id"],
        object_type=req["object_type"],
        operation=req["operation"],
        decision=d["decision"],
        reason_codes=d.get("reason_codes", []),
        ctx_id=result["ctx_id"],
        trace_hash=d["trace_hash"],
        idempotency_key=d["idempotency_key"],
        response_json=result,
    )

    if d["decision"] != "approved":
        return {"ok": False, "governance": result}

    out = mutate_fn()
    return {"ok": True, "data": out, "governance": result}

Framework notes

Kernite is framework-agnostic. This pattern works in FastAPI/Starlette, Django, Flask, internal services, and batch jobs. Choose sink and failure policy based on compliance and availability requirements.