Skip to main content
Restricted Access: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.

Overview

The Audit Diff System records what changed — not just that something changed. When an admin updates a setting, template, or integration rule, the system captures a before/after snapshot and produces a human-readable diff string. This diff is stored in the audit_events table and rendered in the Audit Events page with red (removed) and green (added) highlighting, including word-level change detection.

Storage

The diff string is stored as plain text in the details column (Text, nullable) on the audit_events table. Structured metadata like template_id, template_type, and action goes in the event_metadata column (JSONB). The key columns: The diff string format uses a simple convention:
Lines prefixed with - are removals, + are additions, and non-prefixed lines are section headers (field names). This format is generated by difflib.ndiff on the backend and parsed line-by-line on the frontend.

Frontend Rendering

DiffRenderer.tsx receives the raw details string from GET /org/{org_id}/audit-events and parses it line-by-line: Word-level highlighting: When a removed line (- ) and an added line (+ ) appear near each other (within 3 lines), the component pairs them and runs diffChars (from the diff npm library) to highlight exactly which characters changed within the line. Changed words get a darker background shade — darker green for additions, darker red for removals.

End-to-End Flow

Architecture

The system has three layers. The core engine knows nothing about data sources — adapters bridge the gap between your data format and the engine, and feature modules handle domain-specific logic.

Layer Details

Layer 1: Core Engine — diff_engine.py

A single pure function that does all the diffing:
It takes a list of DiffField descriptors and two objects, reads values from both, and produces a diff string. It has no knowledge of Pydantic, SQLAlchemy, or Cosmos — it works with any object that supports attribute or key access.

Layer 2: Adapters

Each adapter knows how to extract DiffField descriptors from its data source:

Layer 3: Feature Modules

These call the adapters and handle domain-specific logic:

How to Add Audit Logging

Step 1: Choose Your Adapter

  • Pydantic model? → Use model_diff.py (or template_diff.py if it’s a template)
  • SQLAlchemy model? → Use sqlalchemy_diff.py — add info={"diff_label": "..."} to columns you want diffed
  • Cosmos config dict? → Use cosmos_config_diff.py
  • Something custom? → Use diff_engine.py directly with a hand-built DiffField list (see integration_rule_diff.py)

Step 2: Annotate Your Model

Pydantic — all fields are diffed automatically. Override behavior with diff_policy:
SQLAlchemy — only columns with diff_label are diffed:

Step 3: Wire Into the Endpoint

Audit logging must never crash or delay the primary operation. On request paths, capture the old state, complete the primary write, then schedule the audit emitter with create_task_with_error_logging. Keep diff construction inside the async emitter so failures are reported by the task wrapper instead of escaping from the endpoint. If a call site must await audit persistence, wrap that awaited call in try/except instead.

Step 4: Add Auth Context

Most audit endpoints need auth to get user_id and org_id. If the endpoint doesn’t already have it, add it as a parameter:

DiffField Reference

DiffField controls how each field is compared and displayed:

Coverage Matrix


File Index

User and Permission Events

User-management auditing records the administrator who initiated each change in user_id and the affected user or role in the audit entity and metadata. The invite endpoint stores the inviting administrator’s user ID and invite source in server-controlled Stytch trusted metadata. The member.created webhook emits the event after the local user exists, allowing entity_type=USER and entity_id to reference a real user. Duplicate webhook deliveries do not emit duplicate invitation events because the existing-user guards return before emission. Audit failures remain non-blocking: a successful invitation or permission update is not rolled back if its audit event cannot be written.

ATS Write Actions

ATS auditing captures outbound write actions to third-party ATS systems (dispositions, results, notes, field extractions, application creation). High-frequency background polling loops and read operations are intentionally excluded to keep the audit log focused on user- and decision-impacting mutation events. Every ATS audit emitter uses safe_emit_audit_event and resolves entity references (CAMPAIGN, CANDIDATE, INTERVIEW) to provide clickable links in the audit UI.