diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 55be8df7935f3f2709cdbeeb5443a39004f464b7..5e9f800364ebe92891218cfa3213711f45956bfb 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,18 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Deprecated
+~~~~~~~~~~
+
+* Numeric object IDs (primary keys) will be removed and replaced by UUIDs in
+  AlekSIS-Core 4.0. All apps must update their URLs, foriegn keys and the
+  like.
+
+Added
+~~~~~
+
+* Introduce a UUID for every object
+
 `2.10`_ - 2022-06-25
 --------------------
 
diff --git a/aleksis/core/migrations/0042_add_uuid_field.py b/aleksis/core/migrations/0042_add_uuid_field.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f8437c69f049de3ba15f6faad129e64036ca132
--- /dev/null
+++ b/aleksis/core/migrations/0042_add_uuid_field.py
@@ -0,0 +1,95 @@
+# Generated by Django 3.2.14 on 2022-07-16 12:49
+
+import aleksis.core.mixins
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0041_update_gender_choices'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='activity',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='additionalfield',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='announcement',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='announcementrecipient',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='custommenu',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='custommenuitem',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='dashboardwidgetorder',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='datacheckresult',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='grouptype',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='notification',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='pdffile',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='person',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='persongroupthrough',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='schoolterm',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+        migrations.AddField(
+            model_name='taskuserassignment',
+            name='uuid',
+            field=models.UUIDField(default=aleksis.core.mixins._generate_object_uuid, editable=False, unique=True),
+        ),
+    ]
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index c6cee9a8c6bbefed3439eef156b685f5484e4a13..d81c736973aaa27efba672461fd68a67379d5eee 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -1,6 +1,7 @@
 # flake8: noqa: DJ12
 
 import os
+import uuid
 from datetime import datetime
 from typing import Any, Callable, List, Optional, Union
 
@@ -82,6 +83,10 @@ def _generate_one_to_one_proxy_property(field, subfield):
     return property(getter, setter)
 
 
+def _generate_object_uuid():
+    return uuid.uuid5(uuid.NAMESPACE_URL, settings.BASE_URL)
+
+
 class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     """Base model for all objects in AlekSIS apps.
 
@@ -126,6 +131,8 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         - Dominik George <dominik.george@teckids.org>
     """
 
+    uuid = models.UUIDField(unique=True, default=_generate_object_uuid, editable=False)
+
     # Defines a material design icon associated with this type of model
     icon_ = "radio_button_unchecked"