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 theaudit_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 thedetails 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:
- 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:
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 extractDiffField 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(ortemplate_diff.pyif it’s a template) - SQLAlchemy model? → Use
sqlalchemy_diff.py— addinfo={"diff_label": "..."}to columns you want diffed - Cosmos config dict? → Use
cosmos_config_diff.py - Something custom? → Use
diff_engine.pydirectly with a hand-builtDiffFieldlist (seeintegration_rule_diff.py)
Step 2: Annotate Your Model
Pydantic — all fields are diffed automatically. Override behavior withdiff_policy:
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 withcreate_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 needauth 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 inuser_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.