diff --git a/aleksis/core/migrations/0005_timestamped_activity_notification.py b/aleksis/core/migrations/0005_timestamped_activity_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..611c55196dc8c703662380e7c73ca0ae20a42059
--- /dev/null
+++ b/aleksis/core/migrations/0005_timestamped_activity_notification.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.1.3 on 2020-11-16 13:04
+
+from django.db import migrations, models
+import django.utils.timezone
+import model_utils.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0004_add_permissions_for_group_stats'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='activity',
+            name='created',
+            field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
+        ),
+        migrations.AddField(
+            model_name='activity',
+            name='modified',
+            field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
+        ),
+        migrations.AddField(
+            model_name='notification',
+            name='created',
+            field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
+        ),
+        migrations.AddField(
+            model_name='notification',
+            name='modified',
+            field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
+        ),
+    ]
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index e3f36630b169e912ceea50e8b759f4f078459b36..e44caa020bdb6fb1814a3b7e10de95d72915b26b 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -19,7 +19,6 @@ from django.views.generic import CreateView, UpdateView
 from django.views.generic.edit import DeleteView, ModelFormMixin
 
 import reversion
-from easyaudit.models import CRUDEvent
 from guardian.admin import GuardedModelAdmin
 from jsonstore.fields import IntegerField, JSONFieldMixin
 from material.base import Layout, LayoutNode
@@ -54,8 +53,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     This base model ensures all objects in AlekSIS apps fulfill the
     following properties:
 
-     * crud_events property to retrieve easyaudit's CRUD event log
-     * created_at and updated_at properties based n CRUD events
+     * `versions` property to retrieve all versions of the model from reversion
      * Allow injection of fields and code from AlekSIS apps to extend
        model functionality.
 
@@ -109,50 +107,30 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         pass
 
     @property
-    def crud_events(self) -> QuerySet:
-        """Get all CRUD events connected to this object from easyaudit."""
-        content_type = ContentType.objects.get_for_model(self)
+    def versions(self) -> List[Tuple[str, Tuple[Any, Any]]]:
+        """Get all versions of this object from django-reversion.
 
-        return CRUDEvent.objects.filter(
-            object_id=self.pk, content_type=content_type
-        ).select_related("user", "user__person")
+        Includes diffs to previous version.
+        """
+        versions = reversion.models.Version.objects.get_for_object(self)
 
-    @property
-    def crud_event_create(self) -> Optional[CRUDEvent]:
-        """Return create event of this object."""
-        return self.crud_events.filter(event_type=CRUDEvent.CREATE).latest("datetime")
+        versions_with_changes = []
+        for i, version in enumerate(versions):
+            diff = {}
+            if i > 0:
+                prev_version = versions[i - 1]
 
-    @property
-    def crud_event_update(self) -> Optional[CRUDEvent]:
-        """Return last event of this object."""
-        return self.crud_events.latest("datetime")
+                for k, val in version.field_dict.items():
+                    prev_val = prev_version.field_dict.get(k, None)
+                    if prev_val != val:
+                        diff[k] = (prev_val, val)
 
-    @property
-    def created_at(self) -> Optional[datetime]:
-        """Determine creation timestamp from CRUD log."""
-        if self.crud_event_create:
-            return self.crud_event_create.datetime
+            versions_with_changes.append((version, diff))
 
-    @property
-    def updated_at(self) -> Optional[datetime]:
-        """Determine last timestamp from CRUD log."""
-        if self.crud_event_update:
-            return self.crud_event_update.datetime
+        return versions_with_changes
 
     extended_data = JSONField(default=dict, editable=False)
 
-    @property
-    def created_by(self) -> Optional[models.Model]:
-        """Determine user who created this object from CRUD log."""
-        if self.crud_event_create:
-            return self.crud_event_create.user
-
-    @property
-    def updated_by(self) -> Optional[models.Model]:
-        """Determine user who last updated this object from CRUD log."""
-        if self.crud_event_update:
-            return self.crud_event_update.user
-
     extended_data = JSONField(default=dict, editable=False)
 
     @classmethod
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 78bda3bd1068fd441f1fbd50f4bd133e95f0a8dc..c99820e5123f1a2d114c41533a9ee9a6628827d3 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _
 import jsonstore
 from cache_memoize import cache_memoize
 from dynamic_preferences.models import PerInstancePreferenceModel
+from model_utils.models import TimeStampedModel
 from phonenumber_field.modelfields import PhoneNumberField
 from polymorphic.models import PolymorphicModel
 
@@ -440,7 +441,7 @@ class PersonGroupThrough(ExtensibleModel):
             setattr(self, field_name, field_instance)
 
 
-class Activity(ExtensibleModel):
+class Activity(ExtensibleModel, TimeStampedModel):
     """Activity of a user to trace some actions done in AlekSIS in displayable form."""
 
     user = models.ForeignKey(
@@ -460,7 +461,7 @@ class Activity(ExtensibleModel):
         verbose_name_plural = _("Activities")
 
 
-class Notification(ExtensibleModel):
+class Notification(ExtensibleModel, TimeStampedModel):
     """Notification to submit to a user."""
 
     sender = models.CharField(max_length=100, verbose_name=_("Sender"))
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index a04471a0084c16919a0395c2df4ff25cc46aea9f..cd92f28e4a3ef71bcb6220711db196452641d3cc 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -64,7 +64,6 @@ INSTALLED_APPS = [
     "dbbackup",
     "settings_context_processor",
     "sass_processor",
-    "easyaudit",
     "django_any_js",
     "django_yarnpkg",
     "django_tables2",
@@ -129,7 +128,6 @@ MIDDLEWARE = [
     "impersonate.middleware.ImpersonateMiddleware",
     "django.contrib.messages.middleware.MessageMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
-    "easyaudit.middleware.easyaudit.EasyAuditMiddleware",
     "maintenance_mode.middleware.MaintenanceModeMiddleware",
     "aleksis.core.util.middlewares.EnsurePersonMiddleware",
     "django_prometheus.middleware.PrometheusAfterMiddleware",
diff --git a/aleksis/core/templates/core/index.html b/aleksis/core/templates/core/index.html
index 683adca900d46a8ba50790829db4d3ecd0ba2716..bcf0bf1b4180756d53f1519b8ee2371ee3e4e658 100644
--- a/aleksis/core/templates/core/index.html
+++ b/aleksis/core/templates/core/index.html
@@ -48,7 +48,7 @@
                 <span class="badge new primary-color">{{ activity.app }}</span>
                 <span class="title">{{ activity.title }}</span>
                 <p>
-                  <i class="material-icons left">access_time</i> {{ activity.created_at }}
+                  <i class="material-icons left">access_time</i> {{ activity.created }}
                 </p>
                 <p>
                   {{ activity.description }}
@@ -71,7 +71,7 @@
                 <span class="badge new primary-color">{{ notification.app }}</span>
                 <span class="title">{{ notification.title }}</span>
                 <p>
-                  <i class="material-icons left">access_time</i> {{ notification.created_at }}
+                  <i class="material-icons left">access_time</i> {{ notification.created }}
                 </p>
                 <p>
                   {{ notification.description }}
diff --git a/aleksis/core/templates/core/partials/crud_events.html b/aleksis/core/templates/core/partials/crud_events.html
index 50e4f73cab492804b163b7ddfe0dda08b9e267b7..b0edf14d154690e33a2fac523f5fa38429fcdb5e 100644
--- a/aleksis/core/templates/core/partials/crud_events.html
+++ b/aleksis/core/templates/core/partials/crud_events.html
@@ -1,62 +1,32 @@
 {% load i18n data_helpers %}
 
-<ul class="collection">
-  {% for event in obj.crud_events %}
-    {% if no_m2m and event.event_type == event.M2M_CHANGE or event.event_type == event.M2M_CHANGE_REV %}
-    {% else %}
-      <li class="collection-item">
-        <strong>
-          {% if event.event_type == event.CREATE %}
-            {% blocktrans with person=event.user.person %}
-              Created by {{ person }}
-            {% endblocktrans %}
-          {% elif event.event_type == event.UPDATE %}
-            {% blocktrans with person=event.user.person %}
-              Updated by {{ person }}
-            {% endblocktrans %}
-          {% elif event.event_type == event.DELETE %}
-            {% blocktrans with person=event.user.person %}
-              Deleted by {{ person }}
-            {% endblocktrans %}
-          {% elif event.event_type == event.M2M_CHANGE %}
-            {% blocktrans with person=event.user.person %}
-              Updated by {{ person }}
-            {% endblocktrans %}
-          {% elif event.event_type == event.M2M_CHANGE_REV %}
-            {% blocktrans with person=event.user.person %}
-              Updated by {{ person }}
-            {% endblocktrans %}
-          {% endif %}
-        </strong>
-
-        <div class="left" style="margin-right: 10px;">
-          {% if event.event_type == event.CREATE %}
-            <i class="material-icons">add_circle</i>
-          {% elif event.event_type == event.UPDATE %}
-            <i class="material-icons">edit</i>
-          {% elif event.event_type == event.DELETE %}
-            <i class="material-icons">delete</i>
-          {% elif event.event_type == event.M2M_CHANGE %}
-            <i class="material-icons">edit</i>
-          {% elif event.event_type == event.M2M_CHANGE_REV %}
-            <i class="material-icons">edit</i>
-          {% endif %}
-        </div>
-        <div class="right">
-          {{ event.datetime }}
-        </div>
-        {% parse_json event.changed_fields as changed_fields %}
-        {% if changed_fields %}
-          <ul>
-            {% for field, change in changed_fields.items %}
-              {% verbose_name event.content_type.app_label event.content_type.model field as verbose_name %}
-              <li>
-                {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }}
-              </li>
-            {% endfor %}
-          </ul>
+<div class="collection">
+  {% for version in obj.versions %}
+    <div class="collection-item">
+      <div class="left" style="margin-right: 10px;">
+        {% if forloop.first %}
+          <i class="material-icons">add_circle</i>
+        {% else %}
+          <i class="material-icons">edit</i>
         {% endif %}
-      </li>
-    {% endif %}
+      </div>
+      <strong>
+        {{ version.0.revision.get_comment }}
+        {% trans "Changed by" %} {% firstof version.0.revision.user.person _("Unknown") %}
+      </strong>
+      <div class="right">
+        {{ version.0.revision.date_created }}
+      </div>
+      {% if version.1 %}
+        <ul>
+          {% for field, change in version.1.items %}
+            {% verbose_name version.0.content_type.app_label version.0.content_type.model field as verbose_name %}
+            <li>
+              {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }}
+            </li>
+          {% endfor %}
+        </ul>
+      {% endif %}
+    </div>
   {% endfor %}
-</ul>
+</div>
diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email
index 8e61fd0df28b5bc6342c891921831e147e9cc8d9..0e5d9bdee3c8bd9ec3df5c9d008ae68d1f274c23 100644
--- a/aleksis/core/templates/templated_email/notification.email
+++ b/aleksis/core/templates/templated_email/notification.email
@@ -15,7 +15,7 @@
         {% trans "More information" %} → {{ notification.link }}
     {% endif %}
 
-    {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %}
+    {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %}
         Sent by {{ trans_sender }} at {{ trans_created_at }}
     {% endblocktrans %}
 
@@ -37,7 +37,7 @@
     </blockquote>
 
     <p>
-        {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %}
+        {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created %}
             Sent by {{ trans_sender }} at {{ trans_created_at }}
         {% endblocktrans %}
     </p>
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index fc1b0ecb8fdc577d9edb951afe3b25bab1cf9a54..c6df78ecb19284975d339ddedd5439f7515bd7ad 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -17,6 +17,7 @@ from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.views import SearchView
 from health_check.views import MainView
+from reversion import set_user
 from rules.contrib.views import PermissionRequiredMixin, permission_required
 
 from .filters import GroupFilter, PersonFilter
@@ -308,6 +309,7 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse
     if request.method == "POST":
         if edit_person_form.is_valid():
             with reversion.create_revision():
+                set_user(request.user)
                 edit_person_form.save(commit=True)
             messages.success(request, _("The person has been saved."))
 
@@ -344,6 +346,7 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     if request.method == "POST":
         if edit_group_form.is_valid():
             with reversion.create_revision():
+                set_user(request.user)
                 group = edit_group_form.save(commit=True)
 
             messages.success(request, _("The group has been saved."))
@@ -543,6 +546,7 @@ def delete_person(request: HttpRequest, id_: int) -> HttpResponse:
     person = objectgetter_optional(Person)(request, id_)
 
     with reversion.create_revision():
+        set_user(request.user)
         person.save()
 
     person.delete()
@@ -556,6 +560,7 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an group."""
     group = objectgetter_optional(Group)(request, id_)
     with reversion.create_revision():
+        set_user(request.user)
         group.save()
 
     group.delete()
diff --git a/poetry.lock b/poetry.lock
index 94c890d7af8bd0f1b95e28052d208fd621ffdc6d..bf84fb1291e9e0661d08cbbb2629bb2275d0f0b0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -122,21 +122,6 @@ PyYAML = ">=3.13"
 six = ">=1.10.0"
 stevedore = ">=1.20.0"
 
-[[package]]
-name = "beautifulsoup4"
-version = "4.9.3"
-description = "Screen-scraping library"
-category = "main"
-optional = false
-python-versions = "*"
-
-[package.dependencies]
-soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""}
-
-[package.extras]
-html5lib = ["html5lib"]
-lxml = ["lxml"]
-
 [[package]]
 name = "billiard"
 version = "3.6.3.0"
@@ -524,18 +509,6 @@ django = ">=1.11"
 persisting-theory = ">=0.2.1"
 six = "*"
 
-[[package]]
-name = "django-easy-audit"
-version = "1.3.1a1"
-description = "Yet another Django audit log app, hopefully the simplest one."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
-[package.dependencies]
-beautifulsoup4 = "*"
-django = ">=2.2,<3.2"
-
 [[package]]
 name = "django-favicon-plus-reloaded"
 version = "1.0.4"
@@ -703,6 +676,17 @@ python-versions = "*"
 [package.dependencies]
 django = "*"
 
+[[package]]
+name = "django-model-utils"
+version = "4.0.0"
+description = "Django model mixins and utilities"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+Django = ">=2.0.1"
+
 [[package]]
 name = "django-otp"
 version = "1.0.2"
@@ -1808,14 +1792,6 @@ category = "dev"
 optional = false
 python-versions = "*"
 
-[[package]]
-name = "soupsieve"
-version = "2.0.1"
-description = "A modern CSS selector implementation for Beautiful Soup."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-
 [[package]]
 name = "spdx-license-list"
 version = "0.5.1"
@@ -2170,11 +2146,6 @@ bandit = [
     {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"},
     {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"},
 ]
-beautifulsoup4 = [
-    {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
-    {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
-    {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
-]
 billiard = [
     {file = "billiard-3.6.3.0-py3-none-any.whl", hash = "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede"},
     {file = "billiard-3.6.3.0.tar.gz", hash = "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"},
@@ -2327,10 +2298,6 @@ django-dynamic-preferences = [
     {file = "django-dynamic-preferences-1.10.1.tar.gz", hash = "sha256:e4b2bb7b2563c5064ba56dd76441c77e06b850ff1466a386a1cd308909a6c7de"},
     {file = "django_dynamic_preferences-1.10.1-py2.py3-none-any.whl", hash = "sha256:9419fa925fd2cbb665269ae72059eb3058bf080913d853419b827e4e7a141902"},
 ]
-django-easy-audit = [
-    {file = "django-easy-audit-1.3.1a1.tar.gz", hash = "sha256:1aaa7f19a5a6d7f31698661b061e662df50d2506e0828a1cfb681a95c3b34fea"},
-    {file = "django_easy_audit-1.3.1a1-py3-none-any.whl", hash = "sha256:64448dce510673939825b6d5dec674f6c2ac069ab4b4b95cff7f3f796da7c786"},
-]
 django-favicon-plus-reloaded = [
     {file = "django-favicon-plus-reloaded-1.0.4.tar.gz", hash = "sha256:90c761c636a338e6e9fb1d086649d82095085f92cff816c9cf074607f28c85a5"},
     {file = "django_favicon_plus_reloaded-1.0.4-py3-none-any.whl", hash = "sha256:26e4316d41328a61ced52c7fc0ead795f0eb194d6a30311c34a9833c6fe30a7c"},
@@ -2390,6 +2357,10 @@ django-menu-generator = [
 django-middleware-global-request = [
     {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"},
 ]
+django-model-utils = [
+    {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"},
+    {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"},
+]
 django-otp = [
     {file = "django-otp-1.0.2.tar.gz", hash = "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"},
     {file = "django_otp-1.0.2-py3-none-any.whl", hash = "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18"},
@@ -2966,10 +2937,6 @@ snowballstemmer = [
     {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
     {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
 ]
-soupsieve = [
-    {file = "soupsieve-2.0.1-py3-none-any.whl", hash = "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55"},
-    {file = "soupsieve-2.0.1.tar.gz", hash = "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"},
-]
 spdx-license-list = [
     {file = "spdx_license_list-0.5.1-py3-none-any.whl", hash = "sha256:32f1401e0077b46ba8b3d9c648b6503ef1d49c41aab51aa13816be2dde3b4a13"},
     {file = "spdx_license_list-0.5.1.tar.gz", hash = "sha256:64cb5de37724c64cdeccafa2ae68667ff8ccdb7b688f51c1c2be82d7ebe3a112"},
diff --git a/pyproject.toml b/pyproject.toml
index 866a9f3c9e700be11c9e5013537e58c48dfc1d6f..f600af4628071fcbb4b9cbc728e213d70724f4e0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,6 @@ python = "^3.7"
 Django = "^3.0"
 django-any-js = "^1.0"
 django-debug-toolbar = "^2.0"
-django-easy-audit = {version ="^1.2rc1", allow-prereleases = true}
 django-middleware-global-request = "^0.1.2"
 django-menu-generator = "^1.0.4"
 django-tables2 = "^2.1"
@@ -92,6 +91,7 @@ psutil = "^5.7.0"
 celery-progress = "^0.0.14"
 django-prometheus = "^2.1.0"
 importlib-metadata = {version = "^2.0.0", python = "<3.9"}
+django-model-utils = "^4.0.0"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]