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:
ColumnTypeWhat it stores
event_typeString(64)Category — e.g., SETTINGS_UPDATED
entity_typeString(32)What was changed — e.g., ORGANIZATION
entity_idUUIDID of the changed entity
detailsTextThe diff string (plain text with - /+ line prefixes)
event_metadataJSONBStructured context — template type, action, IDs
user_idUUIDWho made the change
The diff string format uses a simple convention:
Field Label
- old value
+ new value

Another Field
- removed line
+ added line
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:
Line patternRendered as
Starts with + Green background, dark green text (addition)
Starts with - Red background, dark red text (removal)
Non-empty, no prefixBold section header (field name)
Matches id: ...Dimmed metadata text
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:
def build_diff(old, new, fields: list[DiffField]) -> str | None
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:
AdapterData SourceHow fields are defined
model_diff.pyPydantic modelsReads json_schema_extra={"diff_policy": {...}} from model fields. All fields are diffed by default.
sqlalchemy_diff.pySQLAlchemy modelsReads info={"diff_label": "..."} from column definitions. Only columns with diff_label are diffed (opt-in).
cosmos_config_diff.pyCosmos config dictsReads from UIConfigModel classes. Converts values to display format via to_ui_schema() (e.g., 0.880 for percentages).

Layer 3: Feature Modules

These call the adapters and handle domain-specific logic:
ModuleWhat it diffs
template_diff.pyAll 8 template types (uses Pydantic adapter)
script_diff.pyCampaign script fields + screening questions (uses Pydantic adapter)
admin_settings_diff.pyATS integration config (uses SQLAlchemy adapter)
integration_rule_diff.pyRules, actions, priorities (uses diff engine directly)
template_audit.pyNot a diff module — shared helper that emits audit events for templates

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:
class MyTemplate(CosmosBaseModel):
    title: str = ""                    # Diffed, auto-labeled as "Title"
    instructions: str = ""             # Diffed, auto-labeled as "Instructions"
    internal_id: str = Field(          # Skipped
        default="",
        json_schema_extra={"diff_policy": {"skip": True}}
    )
    items: list[Item] = Field(         # List with key matching
        default_factory=list,
        json_schema_extra={"diff_policy": {"list_key": "id", "ordered": True}}
    )
SQLAlchemy — only columns with diff_label are diffed:
start_stage: Mapped[str | None] = mapped_column(
    String(255), nullable=True, info={"diff_label": "Start Stage"}
)
# Columns without diff_label are ignored by the diff system

Step 3: Wire Into the Endpoint

Always wrap audit code in try/except — audit logging should never crash the main operation.
# UPDATE
old = await dao.fetch_item(item_id=id, ...)    # 1. Capture old state
await dao.patch_item(item_id=id, ...)           # 2. Do the update
try:                                            # 3. Diff and audit
    updated = await dao.fetch_item(item_id=id, ...)
    details = build_my_diff(old, updated)
    await emit_template_audit(org_id=..., user_id=..., ...)
except Exception:
    logger.warning("Failed to create audit event for ...")
# CREATE
await dao.create_item(item=new_item, ...)       # 1. Create
try:                                            # 2. Diff (old=None) and audit
    details = build_my_diff(None, new_item)
    await emit_template_audit(org_id=..., action=AuditAction.CREATE, ...)
except Exception:
    logger.warning("Failed to create audit event for ...")
# DELETE
old = await dao.fetch_item(item_id=id, ...)     # 1. Capture before delete
await dao.delete_item(item_id=id, ...)          # 2. Delete
try:                                            # 3. Diff (new=None) and audit
    details = build_my_diff(old, None)
    await emit_template_audit(org_id=..., action=AuditAction.DELETE, ...)
except Exception:
    logger.warning("Failed to create audit event for ...")

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:
auth: Annotated[AuthContext, Depends(get_auth_context)]

DiffField Reference

DiffField controls how each field is compared and displayed:
PropertyTypeDefaultDescription
namestrrequiredField name to read from the object
labelstr | NoneNoneHuman-readable label. If None, auto-generated from name (my_field → “My Field”, callIntro → “Call Intro”)
skipboolFalseSkip this field entirely (for internal IDs, etc.)
orderedbool | NoneNoneFor lists: does order matter? True = compare by position, False = sort before comparing, None = treat as ordered
indexedboolFalseFor lists: show [1], [2] prefixes
list_keystr | NoneNoneFor list-of-models: match items by this field instead of position (e.g., "id")
annotationtype | NoneNonePython type hint, used to pick formatting (str → text diff, bool → true/false, int/float → number)
nested_buildercallable | NoneNoneFor nested Pydantic models: function to recursively diff the nested object

Coverage Matrix

AreaCreateUpdateDelete
ATS Integration Config
Dynamic ATS Config (Cosmos)✅ (reset)
Integration Rules
Rule Priorities
Campaign Scripts
Background Info Templates
Script Generation Templates
Summary Templates
Thank You Email Templates
Rejection Email Templates
Custom Booking Outro Templates
Redaction Templates
Suggested Follow Up Templates

File Index

FilePurpose
audit/diff_engine.pyCore engine — build_diff(), DiffField, formatting
audit/model_diff.pyPydantic adapter — extracts DiffField from model fields
audit/sqlalchemy_diff.pySQLAlchemy adapter — extracts DiffField from column info
audit/cosmos_config_diff.pyCosmos adapter — extracts from UIConfigModel classes, converts display values
audit/admin_settings_diff.pyWrapper for AtsIntegrationConfig diff
audit/script_diff.pyCampaign script diff (screening questions + model fields)
audit/integration_rule_diff.pyIntegration rule CRUD + actions + priority reordering
audit/template_diff.pyDiff builders for all 8 template types
audit/template_audit.pyemit_template_audit() — shared helper to create audit events
audit/enums.pyAuditTemplateType, AuditAction, AuditEventType, AuditEntityType
audit/diff_helpers/Low-level formatters: text diff, line diff, bool/number/list formatting
ui/.../DiffRenderer.tsxFrontend — renders diff strings with red/green + word highlighting