diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index aaee34bc1439fb045626ec07768f7b9555c67ef7..5b87a04501275c8941d01277b6afedc7f40e8656 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -1,8 +1,5 @@
-from django.apps import AppConfig, apps
-
-from constance.signals import config_updated
-
 from .signals import clean_scss
+from .util.apps import AppConfig
 
 
 class CoreConfig(AppConfig):
@@ -10,5 +7,7 @@ class CoreConfig(AppConfig):
     verbose_name = "AlekSIS — The Free School Information System"
 
     def ready(self) -> None:
+        super().ready()
+
+    def config_updated(self, *args, **kwargs) -> None:
         clean_scss()
-        config_updated.connect(clean_scss)
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index f98649cdc261b21443cfc1696af2453c119b6f79..bf12d1af56f654e3ea49ef1e79134a4ac689ff34 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -1,6 +1,10 @@
 from importlib import import_module
+from typing import Any, Optional
 
 import django.apps
+from django.db.models.signals import post_migrate, pre_migrate
+
+from constance.signals import config_updated
 
 
 class AppConfig(django.apps.AppConfig):
@@ -17,3 +21,64 @@ class AppConfig(django.apps.AppConfig):
         except ImportError:
             # ImportErrors are non-fatal because model extensions are optional.
             pass
+
+        # Register default listeners
+        pre_migrate.connect(self.pre_migrate, sender=self)
+        post_migrate.connect(self.post_migrate, sender=self)
+        config_updated.connect(self.config_updated)
+
+        # Getting an app ready means it should look at its config once
+        self.config_updated()
+
+    def config_updated(
+        self,
+        key: Optional[str] = "",
+        old_value: Optional[Any] = None,
+        new_value: Optional[Any] = None,
+        **kwargs
+    ) -> None:
+        """ Called on every app instance if a Constance config chagnes, and once on startup
+
+        By default, it does nothing.
+        """
+        pass
+
+    def pre_migrate(
+        self,
+        app_config: django.apps.AppConfig,
+        verbosity: int,
+        interactive: bool,
+        using: str,
+        plan: List[Tuple],
+        apps: django.apps.Apps,
+    ) -> None:
+        """ Called on every app instance before its models are migrated
+
+        By default, it does nothing.
+        """
+        pass
+
+    def post_migrate(
+        self,
+        app_config: django.apps.AppConfig,
+        verbosity: int,
+        interactive: bool,
+        using: str,
+        plan: List[Tuple],
+        apps: django.apps.Apps,
+    ) -> None:
+        """ Called on every app instance after its models have been migrated
+
+        By default, asks all models to do maintenance on their default data.
+        """
+        self._maintain_default_data()
+
+    def _maintain_default_data(self):
+        if not self.models_module:
+            # This app does not have any models, so bail out early
+            return
+
+        for model in self.get_models():
+            if hasattr(model, "maintain_default_data"):
+                # Method implemented by each model object; can be left out
+                model.maintain_default_data()