diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6efe90b75c7e1ae7ba4a387bcc08417172eab585..cc5a940d65188c400ca888031fe4fe18db0cfff2 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,7 @@ Added
 
 * Notification drawer in top nav bar
 * GraphQL queries and mutations for core data management
+* [Dev] Introduce new mechanism to register classes over all apps.
 
 Changed
 ~~~~~~~
@@ -36,6 +37,7 @@ Changed
 * Incorporate SPDX license list for app licenses on About page
 * [Dev] The undocumented field `check` on `DataCheckResult` was renamed to `data_check`
 * Frontend bundling migrated from Webpack to Vite
+* Get dashboard widgets and data checks from apps with new registration mechanism.
 
 Fixed
 ~~~~~
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 1cc493b7141192ba2cdc386b8590518061b1faf5..9b0518472c6309d33305188659de22700497aee5 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -65,8 +65,6 @@ class CoreConfig(AppConfig):
         preference_models.register(personpreferencemodel, person_preferences_registry)
         preference_models.register(grouppreferencemodel, group_preferences_registry)
 
-        self._load_data_checks()
-
         from .health_checks import (
             BackupJobHealthCheck,
             DataChecksHealthCheckBackend,
@@ -79,16 +77,6 @@ class CoreConfig(AppConfig):
         plugin_dir.register(MediaBackupAgeHealthCheck)
         plugin_dir.register(BackupJobHealthCheck)
 
-    @classmethod
-    def _load_data_checks(cls):
-        """Get all data checks from all loaded models."""
-        from aleksis.core.data_checks import DataCheckRegistry
-
-        data_checks = set()
-        for model in apps.get_models():
-            data_checks.update(getattr(model, "data_checks", []))
-        DataCheckRegistry.data_checks = data_checks
-
     def preference_updated(
         self,
         sender: Any,
diff --git a/aleksis/core/data_checks.py b/aleksis/core/data_checks.py
index 57db505e65bbf36172979cf12b6b9f46ffd83049..f16ab2e9f0b3b39ea86d9f53112a4619fb960e90 100644
--- a/aleksis/core/data_checks.py
+++ b/aleksis/core/data_checks.py
@@ -13,6 +13,7 @@ from django.utils.translation import gettext as _
 import reversion
 from reversion import set_comment
 
+from .mixins import RegistryObject
 from .util.celery_progress import ProgressRecorder, recorded_task
 from .util.core_helpers import get_site_preferences
 from .util.email import send_email
@@ -65,7 +66,7 @@ class IgnoreSolveOption(SolveOption):
         check_result.save()
 
 
-class DataCheck:
+class DataCheck(RegistryObject):
     """Define a data check.
 
     Data checks should be used to search objects of
@@ -155,7 +156,6 @@ class DataCheck:
     the preference ``Send emails if data checks detect problems``.
     """  # noqa: D412
 
-    name: str = ""
     verbose_name: str = ""
     problem_name: str = ""
 
@@ -225,25 +225,15 @@ class DataCheck:
         # Reset list with existing problems
         cls._current_results = []
 
-
-class DataCheckRegistry:
-    """Create central registry for all data checks in AlekSIS."""
-
-    data_checks: set = set()
-
-    @classproperty
-    def data_checks_by_name(cls):
-        return {check.name: check for check in cls.data_checks}
-
     @classproperty
     def data_checks_choices(cls):
-        return [(check.name, check.verbose_name) for check in cls.data_checks]
+        return [(check.name, check.verbose_name) for check in cls.registered_objects_list]
 
 
 @recorded_task(run_every=timedelta(minutes=15))
 def check_data(recorder: ProgressRecorder):
     """Execute all registered data checks and send email if activated."""
-    for check in recorder.iterate(DataCheckRegistry.data_checks):
+    for check in recorder.iterate(DataCheck.registered_objects_list):
         logging.info(f"Run check: {check.verbose_name}")
         check.run_check_data()
 
@@ -266,7 +256,7 @@ def send_emails_for_data_checks():
         results_with_checks = []
         for result in results_by_check:
             results_with_checks.append(
-                (DataCheckRegistry.data_checks_by_name[result["data_check"]], result["count"])
+                (DataCheck.registered_objects_dict[result["data_check"]], result["count"])
             )
 
         recipient_list = [
@@ -347,3 +337,6 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
     FieldValidationDataCheck.__name__ = model_name + "FieldValidationDataCheck"
 
     return FieldValidationDataCheck
+
+
+field_validation_data_check_factory("core", "CustomMenuItem", "icon")
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index f4ae9a9d0c28f3539b1f4359785db555557c2bbc..4c581a1aaaf4be642223b515c8e339266a9c029f 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -2,7 +2,7 @@
 
 import os
 from datetime import datetime
-from typing import Any, Callable, List, Optional, Union
+from typing import Any, Callable, ClassVar, List, Optional, Union
 
 from django.conf import settings
 from django.contrib import messages
@@ -16,7 +16,7 @@ from django.db.models.fields import CharField, TextField
 from django.forms.forms import BaseForm
 from django.forms.models import ModelForm, ModelFormMetaclass
 from django.http import HttpResponse
-from django.utils.functional import lazy
+from django.utils.functional import classproperty, lazy
 from django.utils.translation import gettext as _
 from django.views.generic import CreateView, UpdateView
 from django.views.generic.edit import DeleteView, ModelFormMixin
@@ -543,3 +543,36 @@ class PublicFilePreferenceMixin(FilePreference):
         return os.path.join(
             self.upload_path, preferences_settings.FILE_PREFERENCE_UPLOAD_DIR, self.identifier()
         )
+
+
+class RegistryObject:
+    """Provide a generic registry to allow registration of subclasses over all apps."""
+
+    _registry: ClassVar[Optional[dict[str, "RegistryObject"]]] = None
+    name: ClassVar[str] = ""
+
+    def __init_subclass__(cls):
+        print("AM I EXECUTED", cls)
+        if getattr(cls, "_registry", None) is None:
+            cls._registry = {}
+        else:
+            if not cls.name:
+                cls.name = cls.__name__
+            cls._register()
+
+    @classmethod
+    def _register(cls):
+        if cls.name and cls.name not in cls._registry:
+            cls._registry[cls.name] = cls
+
+    @classproperty
+    def registered_objects_dict(cls):
+        return cls._registry
+
+    @classproperty
+    def registered_objects_list(cls):
+        return list(cls._registry.values())
+
+    @classmethod
+    def get_object_by_name(cls, name):
+        cls.registered_objects_dict.get(name)
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index e2f348a7ac9becd40cbc64bae7194b6139eba305..5b4a885c91f1c4a5dd68cfb14c4519873e6c762f 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -55,7 +55,6 @@ from polymorphic.models import PolymorphicModel
 from aleksis.core.data_checks import (
     BrokenDashboardWidgetDataCheck,
     DataCheck,
-    DataCheckRegistry,
     field_validation_data_check_factory,
 )
 
@@ -72,6 +71,7 @@ from .mixins import (
     ExtensibleModel,
     GlobalPermissionModel,
     PureDjangoModel,
+    RegistryObject,
     SchoolTermRelatedExtensibleModel,
 )
 from .tasks import send_notification
@@ -915,13 +915,11 @@ class AnnouncementRecipient(ExtensibleModel):
         verbose_name_plural = _("Announcement recipients")
 
 
-class DashboardWidget(PolymorphicModel, PureDjangoModel):
+class DashboardWidget(RegistryObject, PolymorphicModel, PureDjangoModel):
     """Base class for dashboard widgets on the index page."""
 
     objects = UninstallRenitentPolymorphicManager()
 
-    data_checks = [BrokenDashboardWidgetDataCheck]
-
     @staticmethod
     def get_media(widgets: Union[QuerySet, Iterable]):
         """Return all media required to render the selected widgets."""
@@ -1074,8 +1072,6 @@ class CustomMenu(ExtensibleModel):
 class CustomMenuItem(ExtensibleModel):
     """Single item in a custom menu."""
 
-    data_checks = [field_validation_data_check_factory("core", "CustomMenuItem", "icon")]
-
     menu = models.ForeignKey(
         CustomMenu, models.CASCADE, verbose_name=_("Menu"), related_name="items"
     )
@@ -1171,7 +1167,7 @@ class DataCheckResult(ExtensibleModel):
     data_check = models.CharField(
         max_length=255,
         verbose_name=_("Related data check task"),
-        choices=DataCheckRegistry.data_checks_choices,
+        choices=DataCheck.data_checks_choices,
     )
 
     content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@@ -1183,7 +1179,7 @@ class DataCheckResult(ExtensibleModel):
 
     @property
     def related_check(self) -> DataCheck:
-        return DataCheckRegistry.data_checks_by_name[self.data_check]
+        return DataCheck.registered_objects_dict[self.data_check]
 
     def solve(self, solve_option: str = "default"):
         self.related_check.solve(self, solve_option)
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 27b99b2677f2d1fb1da8b998eda035daaf98f58f..60d2cd701ca8aa59ed7cdc7a02ca923ddc52d9de 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -60,7 +60,7 @@ from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin, permission_required
 from two_factor.views.core import LoginView as AllAuthLoginView
 
-from aleksis.core.data_checks import DataCheckRegistry, check_data
+from aleksis.core.data_checks import DataCheck, check_data
 
 from .celery import app
 from .decorators import pwa_cache
@@ -835,12 +835,12 @@ class DataCheckView(PermissionRequiredMixin, ListView):
         return (
             DataCheckResult.objects.filter(content_type__app_label__in=apps.app_configs.keys())
             .filter(solved=False)
-            .order_by("check")
+            .order_by("data_check")
         )
 
     def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
         context = super().get_context_data(**kwargs)
-        context["registered_checks"] = DataCheckRegistry.data_checks
+        context["registered_checks"] = DataCheck.registered_objects_list
         return context
 
 
@@ -858,7 +858,7 @@ class RunDataChecks(PermissionRequiredMixin, View):
             progress_title=_("Run data checks …"),
             success_message=_("The data checks were run successfully."),
             error_message=_("There was a problem while running data checks."),
-            back_url=reverse("check_data"),
+            back_url="/data_checks/",
         )
 
 
@@ -900,7 +900,7 @@ class DashboardWidgetListView(PermissionRequiredMixin, SingleTableView):
         context = super().get_context_data(**kwargs)
         context["widget_types"] = [
             (ContentType.objects.get_for_model(m, False), m)
-            for m in DashboardWidget.__subclasses__()
+            for m in DashboardWidget.registered_objects_list
         ]
         return context