diff --git a/authentik/blueprints/v1/importer.py b/authentik/blueprints/v1/importer.py
index 13ee74063..1e7df4b98 100644
--- a/authentik/blueprints/v1/importer.py
+++ b/authentik/blueprints/v1/importer.py
@@ -7,6 +7,8 @@ from dacite.config import Config
 from dacite.core import from_dict
 from dacite.exceptions import DaciteError
 from deepmerge import always_merger
+from django.contrib.auth.models import Permission
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldError
 from django.db.models import Model
 from django.db.models.query_utils import Q
@@ -57,8 +59,11 @@ def excluded_models() -> list[type[Model]]:
     from django.contrib.auth.models import User as DjangoUser
 
     return (
+        # Django only classes
         DjangoUser,
         DjangoGroup,
+        ContentType,
+        Permission,
         # Base classes
         Provider,
         Source,
diff --git a/authentik/enterprise/apps.py b/authentik/enterprise/apps.py
index a0b9bed6d..b2b06306f 100644
--- a/authentik/enterprise/apps.py
+++ b/authentik/enterprise/apps.py
@@ -1,4 +1,6 @@
 """Enterprise app config"""
+from django.conf import settings
+
 from authentik.blueprints.apps import ManagedAppConfig
 
 
@@ -17,3 +19,9 @@ class AuthentikEnterpriseConfig(EnterpriseConfig):
     def reconcile_load_enterprise_signals(self):
         """Load enterprise signals"""
         self.import_module("authentik.enterprise.signals")
+
+    def reconcile_install_middleware(self):
+        """Install enterprise audit middleware"""
+        orig_import = "authentik.events.middleware.AuditMiddleware"
+        new_import = "authentik.enterprise.middleware.EnterpriseAuditMiddleware"
+        settings.MIDDLEWARE = [new_import if x == orig_import else x for x in settings.MIDDLEWARE]
diff --git a/authentik/enterprise/middleware.py b/authentik/enterprise/middleware.py
new file mode 100644
index 000000000..d4f012dcc
--- /dev/null
+++ b/authentik/enterprise/middleware.py
@@ -0,0 +1,96 @@
+"""Enterprise audit middleware"""
+from copy import deepcopy
+from functools import partial
+from typing import Callable
+
+from deepdiff import DeepDiff
+from django.core.files import File
+from django.db import connection
+from django.db.models import Model
+from django.db.models.expressions import BaseExpression, Combinable
+from django.db.models.signals import post_init
+from django.http import HttpRequest, HttpResponse
+
+from authentik.core.models import User
+from authentik.enterprise.models import LicenseKey
+from authentik.events.middleware import AuditMiddleware, should_log_model
+
+
+class EnterpriseAuditMiddleware(AuditMiddleware):
+    """Enterprise audit middleware"""
+
+    _enabled = False
+
+    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
+        super().__init__(get_response)
+        self._enabled = LicenseKey.get_total().is_valid()
+
+    def connect(self, request: HttpRequest):
+        super().connect(request)
+        if not self._enabled:
+            return
+        user = getattr(request, "user", self.anonymous_user)
+        if not user.is_authenticated:
+            user = self.anonymous_user
+        if not hasattr(request, "request_id"):
+            return
+        post_init.connect(
+            partial(self.post_init_handler, user=user, request=request),
+            dispatch_uid=request.request_id,
+            weak=False,
+        )
+
+    def disconnect(self, request: HttpRequest):
+        super().disconnect(request)
+        if not self._enabled:
+            return
+        if not hasattr(request, "request_id"):
+            return
+        post_init.disconnect(dispatch_uid=request.request_id)
+
+    def serialize_simple(self, model: Model) -> dict:
+        data = {}
+        deferred_fields = model.get_deferred_fields()
+        for field in model._meta.concrete_fields:
+            value = None
+            if field.remote_field:
+                continue
+
+            if field.get_attname() in deferred_fields:
+                continue
+
+            field_value = getattr(model, field.attname)
+            if isinstance(value, File):
+                field_value = value.name
+
+            # If current field value is an expression, we are not evaluating it
+            if isinstance(field_value, (BaseExpression, Combinable)):
+                continue
+            field_value = field.to_python(field_value)
+            data[field.name] = deepcopy(field_value)
+        return data
+
+    def post_init_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
+        if not should_log_model(instance):
+            return
+        if hasattr(instance, "_previous_state"):
+            return
+        before = len(connection.queries)
+        setattr(instance, "_previous_state", self.serialize_simple(instance))
+        after = len(connection.queries)
+        if after > before:
+            raise AssertionError("More queries generated by serialize_simple")
+
+    def post_save_handler(
+        self, user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
+    ):
+        thread_kwargs = {}
+        if hasattr(instance, "_previous_state") or created:
+            # Get current state
+            prev_state = getattr(instance, "_previous_state", {})
+            new_state = self.serialize_simple(instance)
+            diff = DeepDiff(prev_state, new_state)
+            thread_kwargs["diff"] = diff
+        return super().post_save_handler(
+            user, request, sender, instance, created, thread_kwargs, **_
+        )
diff --git a/authentik/events/middleware.py b/authentik/events/middleware.py
index 9843402ab..a2120f165 100644
--- a/authentik/events/middleware.py
+++ b/authentik/events/middleware.py
@@ -10,52 +10,36 @@ from django.db.models import Model
 from django.db.models.signals import m2m_changed, post_save, pre_delete
 from django.http import HttpRequest, HttpResponse
 from guardian.models import UserObjectPermission
+from structlog.stdlib import BoundLogger, get_logger
 
-from authentik.core.models import (
-    AuthenticatedSession,
-    Group,
-    PropertyMapping,
-    Provider,
-    Source,
-    User,
-    UserSourceConnection,
-)
+from authentik.blueprints.v1.importer import excluded_models
+from authentik.core.models import Group, User
 from authentik.enterprise.providers.rac.models import ConnectionToken
 from authentik.events.models import Event, EventAction, Notification
 from authentik.events.utils import model_to_dict
-from authentik.flows.models import FlowToken, Stage
 from authentik.lib.sentry import before_send
 from authentik.lib.utils.errors import exception_to_string
-from authentik.outposts.models import OutpostServiceConnection
-from authentik.policies.models import Policy, PolicyBindingModel
 from authentik.policies.reputation.models import Reputation
 from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
 from authentik.providers.scim.models import SCIMGroup, SCIMUser
 from authentik.stages.authenticator_static.models import StaticToken
 
-IGNORED_MODELS = (
-    Event,
-    Notification,
-    UserObjectPermission,
-    AuthenticatedSession,
-    StaticToken,
-    Session,
-    FlowToken,
-    Provider,
-    Source,
-    PropertyMapping,
-    UserSourceConnection,
-    Stage,
-    OutpostServiceConnection,
-    Policy,
-    PolicyBindingModel,
-    AuthorizationCode,
-    AccessToken,
-    RefreshToken,
-    SCIMUser,
-    SCIMGroup,
-    Reputation,
-    ConnectionToken,
+IGNORED_MODELS = tuple(
+    excluded_models()
+    + (
+        Event,
+        Notification,
+        UserObjectPermission,
+        StaticToken,
+        Session,
+        AuthorizationCode,
+        AccessToken,
+        RefreshToken,
+        SCIMUser,
+        SCIMGroup,
+        Reputation,
+        ConnectionToken,
+    )
 )
 
 
@@ -96,9 +80,11 @@ class AuditMiddleware:
 
     get_response: Callable[[HttpRequest], HttpResponse]
     anonymous_user: User = None
+    logger: BoundLogger
 
     def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
         self.get_response = get_response
+        self.logger = get_logger().bind()
 
     def _ensure_fallback_user(self):
         """Defer fetching anonymous user until we have to"""
@@ -116,21 +102,18 @@ class AuditMiddleware:
             user = self.anonymous_user
         if not hasattr(request, "request_id"):
             return
-        post_save_handler = partial(self.post_save_handler, user=user, request=request)
-        pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
-        m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
         post_save.connect(
-            post_save_handler,
+            partial(self.post_save_handler, user=user, request=request),
             dispatch_uid=request.request_id,
             weak=False,
         )
         pre_delete.connect(
-            pre_delete_handler,
+            partial(self.pre_delete_handler, user=user, request=request),
             dispatch_uid=request.request_id,
             weak=False,
         )
         m2m_changed.connect(
-            m2m_changed_handler,
+            partial(self.m2m_changed_handler, user=user, request=request),
             dispatch_uid=request.request_id,
             weak=False,
         )
@@ -173,19 +156,26 @@ class AuditMiddleware:
             )
             thread.run()
 
-    @staticmethod
     def post_save_handler(
-        user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
+        self,
+        user: User,
+        request: HttpRequest,
+        sender,
+        instance: Model,
+        created: bool,
+        thread_kwargs: Optional[dict] = None,
+        **_,
     ):
         """Signal handler for all object's post_save"""
         if not should_log_model(instance):
             return
 
         action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
-        EventNewThread(action, request, user=user, model=model_to_dict(instance)).run()
+        thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
+        thread.kwargs.update(thread_kwargs or {})
+        thread.run()
 
-    @staticmethod
-    def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
+    def pre_delete_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
         """Signal handler for all object's pre_delete"""
         if not should_log_model(instance):  # pragma: no cover
             return
@@ -197,9 +187,8 @@ class AuditMiddleware:
             model=model_to_dict(instance),
         ).run()
 
-    @staticmethod
     def m2m_changed_handler(
-        user: User, request: HttpRequest, sender, instance: Model, action: str, **_
+        self, user: User, request: HttpRequest, sender, instance: Model, action: str, **_
     ):
         """Signal handler for all object's m2m_changed"""
         if action not in ["pre_add", "pre_remove", "post_clear"]:
diff --git a/poetry.lock b/poetry.lock
index 3f21176cd..8ecdd27f9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1057,6 +1057,24 @@ files = [
     {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"},
 ]
 
+[[package]]
+name = "deepdiff"
+version = "6.7.1"
+description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "deepdiff-6.7.1-py3-none-any.whl", hash = "sha256:58396bb7a863cbb4ed5193f548c56f18218060362311aa1dc36397b2f25108bd"},
+    {file = "deepdiff-6.7.1.tar.gz", hash = "sha256:b367e6fa6caac1c9f500adc79ada1b5b1242c50d5f716a1a4362030197847d30"},
+]
+
+[package.dependencies]
+ordered-set = ">=4.0.2,<4.2.0"
+
+[package.extras]
+cli = ["click (==8.1.3)", "pyyaml (==6.0.1)"]
+optimize = ["orjson"]
+
 [[package]]
 name = "deepmerge"
 version = "1.1.1"
@@ -2473,6 +2491,20 @@ files = [
     {file = "opencontainers-0.0.14.tar.gz", hash = "sha256:fde3b8099b56b5c956415df8933e2227e1914e805a277b844f2f9e52341738f2"},
 ]
 
+[[package]]
+name = "ordered-set"
+version = "4.1.0"
+description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
+    {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
+]
+
+[package.extras]
+dev = ["black", "mypy", "pytest"]
+
 [[package]]
 name = "outcome"
 version = "1.3.0.post0"
@@ -4488,4 +4520,4 @@ files = [
 [metadata]
 lock-version = "2.0"
 python-versions = "~3.12"
-content-hash = "6dcbc2c6d02643a72285e075528ec0841b9c8fda244632386ec19efb7350d4cd"
+content-hash = "b5127f147f007d9fd1fa661ae66f02f85d9143dda27e1ea5fe4568230c12b7b2"
diff --git a/pyproject.toml b/pyproject.toml
index e38b0bdbd..c1d02b081 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -125,6 +125,7 @@ channels-redis = "*"
 codespell = "*"
 colorama = "*"
 dacite = "*"
+deepdiff = "*"
 deepmerge = "*"
 defusedxml = "*"
 django = "*"
@@ -151,6 +152,7 @@ lxml = [
     # 4.9.x works with previous libxml2 versions, which is what we get on linux
     { version = "4.9.4", platform = "linux" },
 ]
+jsonpatch = "*"
 opencontainers = { extras = ["reggie"], version = "*" }
 packaging = "*"
 paramiko = "*"
@@ -176,7 +178,6 @@ webauthn = "*"
 wsproto = "*"
 xmlsec = "*"
 zxcvbn = "*"
-jsonpatch = "*"
 
 [tool.poetry.dev-dependencies]
 bandit = "*"
diff --git a/web/src/admin/events/EventViewPage.ts b/web/src/admin/events/EventViewPage.ts
index b5351840a..8e1c12bf7 100644
--- a/web/src/admin/events/EventViewPage.ts
+++ b/web/src/admin/events/EventViewPage.ts
@@ -154,6 +154,12 @@ export class EventViewPage extends AKElement {
                     <div class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl">
                         <ak-event-info .event=${this.event}></ak-event-info>
                     </div>
+                    <div class="pf-c-card pf-l-grid__item pf-m-12-col">
+                        <div class="pf-c-card__title">${msg("Raw event info")}</div>
+                        <div class="pf-c-card__body">
+                            <pre>${JSON.stringify(this.event, null, 4)}</pre>
+                        </div>
+                    </div>
                 </div>
             </section>`;
     }
diff --git a/web/src/components/ak-event-info.ts b/web/src/components/ak-event-info.ts
index e728958c2..a94ec9a41 100644
--- a/web/src/components/ak-event-info.ts
+++ b/web/src/components/ak-event-info.ts
@@ -248,6 +248,7 @@ export class EventInfo extends AKElement {
             <div class="pf-c-card__body">
                 ${this.getModelInfo(this.event.context?.model as EventModel)}
             </div>
+            <ak-expand>${this.renderDefaultResponse()}</ak-expand>
         `;
     }