diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..2885a646cf0cc83ec47f46bc2ba5438886ef1952
--- /dev/null
+++ b/aleksis/core/preferences.py
@@ -0,0 +1,128 @@
+from django.conf import settings
+from django.forms import EmailField, URLField
+from django.utils.translation import gettext_lazy as _
+
+from colorfield.widgets import ColorWidget
+from dynamic_preferences.types import BooleanPreference, StringPreference
+from dynamic_preferences.preferences import Section
+from dynamic_preferences.registries import global_preferences_registry
+from dynamic_preferences.users.registries import user_preferences_registry
+
+
+general = Section("general")
+theme = Section(_("theme")
+mail = Section("mail")
+notification = Section("notification")
+footer = Section("footer")
+account = Section("account")
+
+
+@global_preferences_registry.register
+class SiteTitle(StringPreference):
+    section = general
+    name = "title"
+    default = "AlekSIS"
+    required = False
+    verbose_name = _("Site title")
+
+
+@global_preferences_registry.register
+class SiteDescription(StringPreference):
+    section = general
+    name = "description"
+    default = "The Free School Information System"
+    required = False
+    verbose_name = _("Site description")
+
+
+@global_preferences_registry.register
+class ColourPrimary(StringPreference):
+    section = theme
+    name = "primary"
+    default = "#0d5eaf"
+    required = False
+    verbose_name = _("Primary colour")
+    widget = ColorWidget
+
+
+@global_preferences_registry.register
+class ColourSecondary(StringPreference):
+    section = theme
+    name = "secondary"
+    default = "#0d5eaf"
+    required = False
+    verbose_name = _("Secondary colour")
+    widget = ColorWidget
+
+
+@global_preferences_registry.register
+class MailOutName(StringPreference):
+    section = mail
+    name = "name"
+    default = "AlekSIS"
+    required = False
+    verbose_name = _("Mail out name")
+
+
+@global_preferences_registry.register
+class MailOut(StringPreference):
+    section = mail
+    name = "name"
+    default = settings.DEFAULT_FROM_EMAIL
+    required = False
+    verbose_name = _("Mail out address")
+    widget = EmailField
+
+
+@global_preferences_registry.register
+class PrivacyURL(StringPreference):
+    section = footer
+    name = "privacy_url"
+    default = ""
+    required = False
+    verbose_name = _("Link to privacy policy")
+    widget = URLField
+
+
+@global_preferences_registry.register
+class ImprintURL(StringPreference):
+    section = footer
+    name = "imprint_url"
+    default = ""
+    required = False
+    verbose_name = _("Link to imprint")
+    widget = URLField
+
+
+@user_preferences_registry.register
+class AdressingNameFormat(ChoicePreference):
+    section = notification
+    name = "addressing_name_format"
+    default = "german"
+    required = False
+    verbose_name = _("Name format for addressing")
+    choices = (
+               (None, "-----"),
+               ("german", "John Doe"),
+               ("english", "Doe, John"),
+               ("dutch", "Doe John"),
+              )
+
+
+@user_preferences_registry.register
+class NotificationChannels(MultipleChoicePreference):
+    section = notification
+    name = "channels"
+    default = ["email"]
+    required = False
+    verbose_name = _("Channels to use for notifications")
+    choices = get_notification_choices_lazy
+
+
+@global_preferences_registry.register
+class PrimaryGroupPattern(StringPreference):
+    section = account
+    name = "primary_group_pattern"
+    default = ""
+    required = False
+    verbose_name = _("Regular expression to match primary group, e.g. '^Class .*'")
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 6fd8e14951bb0585f2138a37fe384d726f0b4ccc..42c9a0a4c174dca78fbb0f48a75d132ebc9b450c 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -5,13 +5,11 @@ from importlib import import_module
 
 from django.apps import apps
 from django.utils.translation import gettext_lazy as _
-from calendarweek.django import i18n_day_name_choices_lazy
 
 from dynaconf import LazySettings
 from easy_thumbnails.conf import Settings as thumbnail_settings
 
 from .util.core_helpers import get_app_packages, lazy_config, merge_app_settings
-from .util.notifications import get_notification_choices_lazy
 
 ENVVAR_PREFIX_FOR_DYNACONF = "ALEKSIS"
 DIRS_FOR_DYNACONF = ["/etc/aleksis"]
@@ -66,9 +64,9 @@ INSTALLED_APPS = [
     "settings_context_processor",
     "sass_processor",
     "easyaudit",
-    "constance",
-    "constance.backends.database",
     "django_any_js",
+    "dynamic_preferences",
+    "dynamic_preferences.users.apps.UserPreferencesConfig",
     "django_yarnpkg",
     "django_tables2",
     "easy_thumbnails",
@@ -142,7 +140,7 @@ TEMPLATES = [
                 "django.contrib.messages.context_processors.messages",
                 "maintenance_mode.context_processors.maintenance_mode",
                 "settings_context_processor.context_processors.settings",
-                "constance.context_processors.config",
+                "dynamic_preferences.processors.global_preferences",
                 "aleksis.core.util.core_helpers.custom_information_processor",
             ],
         },
@@ -348,64 +346,9 @@ TEMPLATED_EMAIL_AUTO_PLAIN = True
 
 TEMPLATE_VISIBLE_SETTINGS = ["ADMINS", "DEBUG"]
 
-CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
-CONSTANCE_ADDITIONAL_FIELDS = {
-    "char_field": ["django.forms.CharField", {}],
-    "image_field": ["django.forms.ImageField", {}],
-    "email_field": ["django.forms.EmailField", {}],
-    "url_field": ["django.forms.URLField", {}],
-    "integer_field": ["django.forms.IntegerField", {}],
-    "password_field": ["django.forms.CharField", {
-        'widget': 'django.forms.PasswordInput',
-    }],
-    "adressing-select": ['django.forms.fields.ChoiceField', {
-        'widget': 'django.forms.Select',
-        'choices': ((None, "-----"),
-                    # ("german", _("<first name>") + " " + _("<last name>")),
-                    # ("english", _("<last name>") + ", " + _("<first name>")),
-                    # ("netherlands", _("<last name>") + " " + _("<first name>")),
-                    ("german", "John Doe"),
-                    ("english", "Doe, John"),
-                    ("dutch", "Doe John"),
-                    )
-    }],
-    "notifications-select": ["django.forms.fields.MultipleChoiceField", {
-        "widget": "django.forms.CheckboxSelectMultiple",
-        "choices": get_notification_choices_lazy,
-    }],
-    "weekday_field": ["django.forms.fields.ChoiceField", {
-        'widget': 'django.forms.Select',
-        "choices":  i18n_day_name_choices_lazy
-    }],
-    "colour_field": ["django.forms.CharField", {
-        "widget": "colorfield.widgets.ColorWidget"
-    }],
+DYNAMIC_PREFERENCES = {
+    "REGISTRY_MODULE": "preferences",
 }
-CONSTANCE_CONFIG = {
-    "SITE_TITLE": ("AlekSIS", _("Site title"), "char_field"),
-    "SITE_DESCRIPTION": ("The Free School Information System", _("Site description")),
-    "COLOUR_PRIMARY": ("#0d5eaf", _("Primary colour"), "colour_field"),
-    "COLOUR_SECONDARY": ("#0d5eaf", _("Secondary colour"), "colour_field"),
-    "MAIL_OUT_NAME": ("AlekSIS", _("Mail out name")),
-    "MAIL_OUT": (DEFAULT_FROM_EMAIL, _("Mail out address"), "email_field"),
-    "PRIVACY_URL": ("", _("Link to privacy policy"), "url_field"),
-    "IMPRINT_URL": ("", _("Link to imprint"), "url_field"),
-    "ADRESSING_NAME_FORMAT": ("german", _("Name format of adresses"), "adressing-select"),
-    "NOTIFICATION_CHANNELS": (["email"], _("Channels to allow for notifications"), "notifications-select"),
-    "PRIMARY_GROUP_PATTERN": ("", _("Regular expression to match primary group, e.g. '^Class .*'"), str),
-}
-CONSTANCE_CONFIG_FIELDSETS = {
-    "General settings": ("SITE_TITLE", "SITE_DESCRIPTION"),
-    "Theme settings": ("COLOUR_PRIMARY", "COLOUR_SECONDARY"),
-    "Mail settings": ("MAIL_OUT_NAME", "MAIL_OUT"),
-    "Notification settings": ("NOTIFICATION_CHANNELS", "ADRESSING_NAME_FORMAT"),
-    "Footer settings": ("PRIVACY_URL", "IMPRINT_URL"),
-    "Account settings": ("PRIMARY_GROUP_PATTERN",),
-}
-
-merge_app_settings("CONSTANCE_ADDITIONAL_FIELDS", CONSTANCE_ADDITIONAL_FIELDS, False)
-merge_app_settings("CONSTANCE_CONFIG", CONSTANCE_CONFIG, False)
-merge_app_settings("CONSTANCE_CONFIG_FIELDSETS", CONSTANCE_CONFIG_FIELDSETS, False)
 
 MAINTENANCE_MODE = _settings.get("maintenance.enabled", None)
 MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get(
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 13558233068626c8f288b70624b2646840826819..797b8e25c5dac0a1532619a119234043f79796c4 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -13,6 +13,8 @@ from django.http import HttpRequest
 from django.utils import timezone
 from django.utils.functional import lazy
 
+from dynamic_preferences.registries import global_preferences_registry
+
 
 def copyright_years(years: Sequence[int], seperator: str = ", ", joiner: str = "–") -> str:
     """ Takes a sequence of integegers and produces a string with ranges
@@ -87,17 +89,17 @@ def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: b
 
 
 def lazy_config(key: str) -> Callable[[str], Any]:
-    """ Lazily get a config value from constance. Useful to bind constance
-    configs to other global settings to make them available to third-party
-    apps that are not aware of constance.
+    """ Lazily get a config value from dynamic preferences. Useful to bind preferences
+    to other global settings to make them available to third-party apps that are not
+    aware of dynamic preferences.
     """
 
     def _get_config(key: str) -> Any:
-        from constance import config  # noqa
-        return getattr(config, key)
+        return global_preferences[key]
 
     # The type is guessed from the default value to improve lazy()'s behaviour
-    return lazy(_get_config, type(settings.CONSTANCE_CONFIG[key][0]))(key)
+    # FIXME Reintroduce the behaviour described above
+    return lazy(_get_config, str)(key)
 
 
 def is_impersonate(request: HttpRequest) -> bool:
diff --git a/pyproject.toml b/pyproject.toml
index 118325ec2e47142790983685cc36bb77531ccf03..2dc251884b48139eb293f65e62c8986803cc0394 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,7 +50,7 @@ django-two-factor-auth = { version = "^1.10.0", extras = [ "YubiKey", "phonenumb
 django-yarnpkg = "^6.0"
 django-material = "^1.6.0"
 django-pwa = "^1.0.8"
-django-constance = { version = "^2.6.0", extras = ["database"] }
+django-dynamic-preferences = "^1.8.1"
 django_widget_tweaks = "^1.4.5"
 django-filter = "^2.2.0"
 django-templated-email = "^2.3.0"