diff --git a/invenio_config_tuw/startup.py b/invenio_config_tuw/startup.py
deleted file mode 100644
index d656ba5c7822077d112cb9f4aa09ad9427301af9..0000000000000000000000000000000000000000
--- a/invenio_config_tuw/startup.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2022-2024 TU Wien.
-#
-# Invenio-Config-TUW is free software; you can redistribute it and/or modify
-# it under the terms of the MIT License; see LICENSE file for more details.
-
-"""Invenio-Config-TUW hacks and overrides to be applied on application startup.
-
-This module provides a blueprint whose sole purpose is to execute some code exactly
-once during application startup (via ``bp.record_once()``).
-These functions will be executed after the Invenio modules' extensions have been
-initialized, and thus we can rely on them being already available.
-"""
-
-from logging import ERROR
-from logging.handlers import SMTPHandler
-
-import importlib_metadata
-from flask.config import Config
-from invenio_rdm_records.services.search_params import MyDraftsParam
-from invenio_requests.proxies import current_request_type_registry
-
-from .curations import TUWCurationRequest
-from .logs import DetailedFormatter
-
-
-class TUWConfig(Config):
-    """Override for the Flask config that evaluates the SITE_{API,UI}_URL proxies."""
-
-    @classmethod
-    def from_flask_config(cls, config):
-        """Create a clone of the given config."""
-        if isinstance(config, TUWConfig):
-            return config
-
-        return cls(config.root_path, config)
-
-    def __getitem__(self, key):
-        """Return config[key], or str(config[key]) if key is 'SITE_{UI,API}_URL'."""
-        value = super().__getitem__(key)
-
-        # give special treatment to the URL configuration items:
-        # enforce their evaluation as strings
-        if key in ("SITE_UI_URL", "SITE_API_URL"):
-            value = str(value)
-
-        return value
-
-
-def register_smtp_error_handler(app):
-    """Register email error handler to the application."""
-    handler_name = "invenio-config-tuw-smtp-error-handler"
-
-    # check reasons to skip handler registration
-    error_mail_disabled = app.config.get("CONFIG_TUW_DISABLE_ERROR_MAILS", False)
-    if app.debug or app.testing or error_mail_disabled:
-        # email error handling should occur only in production mode, if not disabled
-        return
-
-    elif any([handler.name == handler_name for handler in app.logger.handlers]):
-        # we don't want to register duplicate handlers
-        return
-
-    elif "invenio-mail" not in app.extensions:
-        app.logger.warning(
-            (
-                "The Invenio-Mail extension is not loaded! "
-                "Skipping registration of SMTP error handler."
-            )
-        )
-        return
-
-    # check if mail server and admin email(s) are present in the config
-    # if not raise a warning
-    if app.config.get("MAIL_SERVER") and app.config.get("MAIL_ADMIN"):
-        # configure auth
-        username = app.config.get("MAIL_USERNAME")
-        password = app.config.get("MAIL_PASSWORD")
-        auth = (username, password) if username and password else None
-
-        # configure TLS
-        secure = None
-        if app.config.get("MAIL_USE_TLS"):
-            secure = ()
-
-        # initialize SMTP Handler
-        mail_handler = SMTPHandler(
-            mailhost=(app.config["MAIL_SERVER"], app.config.get("MAIL_PORT", 25)),
-            fromaddr=app.config["SECURITY_EMAIL_SENDER"],
-            toaddrs=app.config["MAIL_ADMIN"],
-            subject=app.config["THEME_SITENAME"] + " - Failure",
-            credentials=auth,
-            secure=secure,
-        )
-        mail_handler.name = handler_name
-        mail_handler.setLevel(ERROR)
-        mail_handler.setFormatter(DetailedFormatter())
-
-        # attach to the application
-        app.logger.addHandler(mail_handler)
-
-    else:
-        app.logger.warning(
-            "Mail configuration missing: SMTP error handler not registered!"
-        )
-
-
-def override_search_drafts_options(app):
-    """Override the "search drafts" options to show all accessible drafts."""
-    # doing this via config is (currently) not possible, as the `search_drafts`
-    # property can't be overridden with a config item (unlike `search`, above it)
-    # cf. https://github.com/inveniosoftware/invenio-rdm-records/blob/maint-10.x/invenio_rdm_records/services/config.py#L327-L332
-    try:
-        service = app.extensions["invenio-rdm-records"].records_service
-        service.config.search_drafts.params_interpreters_cls.remove(MyDraftsParam)
-    except ValueError:
-        pass
-
-
-def register_menu_entries(app):
-    """Register the curation setting endpoint in Flask-Menu."""
-    menu = app.extensions["menu"].root()
-    menu.submenu("settings.curation").register(
-        "invenio_config_tuw_settings.curation_settings_view",
-        '<i class="file icon"></i> Curation',
-        order=1,
-    )
-
-
-def customize_curation_request_type(app):
-    """Override the rdm-curations request type with our own version."""
-    current_request_type_registry.register_type(TUWCurationRequest(), force=True)
-
-
-def override_flask_config(app):
-    """Replace the app's config with our own override.
-
-    This evaluates the ``LocalProxy`` objects used for ``SITE_{API,UI}_URL`` by
-    casting them into strings (which is their expected type).
-    """
-    app.config = TUWConfig.from_flask_config(app.config)
-
-    # we need to override the "config" global, as that might still be the
-    # old "normal" Flask config, and thus have different content
-    app.add_template_global(app.config, "config")
-
-
-def override_prefixed_config(app):
-    """Override config items with their prefixed siblings' values.
-
-    The prefix is determined via the config item "CONFIG_TUW_CONFIG_OVERRIDE_PREFIX".
-    Configuration items with this prefix will override the values for
-
-    If the prefix is set to `None` (the default), then this feature will be disabled.
-    """
-    prefix = app.config.get("CONFIG_TUW_CONFIG_OVERRIDE_PREFIX", None)
-    if prefix is None:
-        return
-
-    prefix_len = len(prefix)
-    pairs = [(k, v) for k, v in app.config.items() if k.startswith(prefix)]
-
-    for key, value in pairs:
-        key = key[prefix_len:]
-        app.config[key] = value
-
-
-def patch_flask_create_url_adapter(app):
-    """Patch Flask's {host,subdomain}_matching with 3.1 behavior.
-
-    See: https://github.com/pallets/flask/pull/5634
-
-    This can be removed once we get Flask 3.1+ in.
-    """
-    flask_version = importlib_metadata.version("flask").split(".", 2)
-    major = int(flask_version[0])
-    minor = int(flask_version[1])
-    if (major == 3 and minor >= 1) or (major > 3):
-        app.logger.info(
-            f"Flask version is {flask_version} (>= 3.1). "
-            "Skipping monkey patching app.create_url_adapter()."
-        )
-        return
-
-    def create_url_adapter(self, request):
-        """Create a URL adapter for the given request.
-
-        This function is from Flask 3.1, which is licensed under the 3-clause BSD.
-        """
-        if request is not None:
-            subdomain = None
-            server_name = self.config["SERVER_NAME"]
-
-            if self.url_map.host_matching:
-                # Don't pass SERVER_NAME, otherwise it's used and the actual
-                # host is ignored, which breaks host matching.
-                server_name = None
-            elif not self.subdomain_matching:
-                # Werkzeug doesn't implement subdomain matching yet. Until then,
-                # disable it by forcing the current subdomain to the default, or
-                # the empty string.
-                subdomain = self.url_map.default_subdomain or ""
-
-            return self.url_map.bind_to_environ(
-                request.environ, server_name=server_name, subdomain=subdomain
-            )
-
-        # Need at least SERVER_NAME to match/build outside a request.
-        if self.config["SERVER_NAME"] is not None:
-            return self.url_map.bind(
-                self.config["SERVER_NAME"],
-                script_name=self.config["APPLICATION_ROOT"],
-                url_scheme=self.config["PREFERRED_URL_SCHEME"],
-            )
-
-        return None
-
-    app.create_url_adapter = create_url_adapter.__get__(app, type(app))
diff --git a/invenio_config_tuw/startup/__init__.py b/invenio_config_tuw/startup/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d25e9f3b5c77cee36419c7ac7cc3a45527beaf7e
--- /dev/null
+++ b/invenio_config_tuw/startup/__init__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2025 TU Wien.
+#
+# Invenio-Config-TUW is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+
+"""Invenio-Config-TUW hacks and overrides to be applied on application startup.
+
+This module provides a blueprint whose sole purpose is to execute some code exactly
+once during application startup (via ``bp.record_once()``).
+These functions will be executed after the Invenio modules' extensions have been
+initialized, and thus we can rely on them being already available.
+"""
+
+
+from .config import finalize_config, patch_flask_create_url_adapter
+from .misc import (
+    customize_curation_request_type,
+    override_search_drafts_options,
+    register_menu_entries,
+    register_smtp_error_handler,
+)
+
+__all__ = (
+    "customize_curation_request_type",
+    "finalize_config",
+    "override_search_drafts_options",
+    "patch_flask_create_url_adapter",
+    "register_menu_entries",
+    "register_smtp_error_handler",
+)
diff --git a/invenio_config_tuw/startup/config.py b/invenio_config_tuw/startup/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..90781d8bc045b8294f3adf9f209a8172eef14056
--- /dev/null
+++ b/invenio_config_tuw/startup/config.py
@@ -0,0 +1,359 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2022-2025 TU Wien.
+#
+# Invenio-Config-TUW is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+
+"""Invenio-Config-TUW hacks and overrides to be applied on application startup.
+
+This module provides a blueprint whose sole purpose is to execute some code exactly
+once during application startup (via ``bp.record_once()``).
+These functions will be executed after the Invenio modules' extensions have been
+initialized, and thus we can rely on them being already available.
+"""
+
+from functools import partial
+
+import importlib_metadata
+from flask import current_app, request
+from flask.config import Config
+from werkzeug.local import LocalProxy
+
+from ..auth.settings import TUWSSOSettingsHelper
+
+
+class TUWConfig(Config):
+    """Override for the Flask config that evaluates the SITE_{API,UI}_URL proxies."""
+
+    @classmethod
+    def from_flask_config(cls, config):
+        """Create a clone of the given config."""
+        if isinstance(config, TUWConfig):
+            return config
+
+        return cls(config.root_path, config)
+
+    def __getitem__(self, key):
+        """Return config[key], or str(config[key]) if key is 'SITE_{UI,API}_URL'."""
+        value = super().__getitem__(key)
+
+        # give special treatment to the URL configuration items:
+        # enforce their evaluation as strings
+        if key in ("SITE_UI_URL", "SITE_API_URL"):
+            value = str(value)
+
+        return value
+
+
+def _get_config(app, key, prefixes=None, default=None):
+    """Get the config item, preferably with the longest matching prefix."""
+    prefixes = [p for p in (prefixes or []) if p is not None]
+    for prefix in sorted(prefixes, key=len, reverse=True):
+        prefixed_key = prefix + key
+        if prefixed_key in app.config:
+            return app.config[prefixed_key]
+
+    if key in app.config:
+        return app.config[key]
+    else:
+        return default
+
+
+def _make_site_url(suffix):
+    """Create a URL with the given suffix from contextual information.
+
+    If available, use the request's Host URL as base for the URL.
+    Otherwise, look at the configuration value for `THEME_SITEURL`.
+    """
+    url = None
+
+    try:
+        if request and request.host_url and request.host_url.startswith("http"):
+            url = request.host_url
+
+    except RuntimeError:
+        # this will be hit if we're working outside of a request context
+        pass
+
+    # use THEME_SITEURL or relative URLs as fallback
+    if url is None:
+        url = current_app.config.get("THEME_SITEURL", "")
+
+    # do a little dance to make sure there's no extra slashes
+    return (url.rstrip("/") + "/" + suffix.lstrip("/")).rstrip("/")
+
+
+def assemble_db_uri_from_parts(app):
+    """Assemble the DB connection string from its parts."""
+    prefixes = ["SQLALCHEMY_"]
+    db_uri = _get_config(app, "DATABASE_URI", prefixes)
+
+    db_driver = _get_config(
+        app, "DATABASE_DRIVER", prefixes, default="postgresql+psycopg2"
+    )
+    db_user = _get_config(app, "DATABASE_USER", prefixes)
+    db_pw = _get_config(app, "DATABASE_PASSWORD", prefixes)
+    db_host = _get_config(app, "DATABASE_HOST", prefixes, default="localhost")
+    db_db = _get_config(app, "DATABASE_DB", prefixes, default=db_user)
+
+    if all((v is not None for v in [db_driver, db_user, db_pw, db_host, db_db])):
+        db_uri = f"{db_driver}://{db_user}:{db_pw}@{db_host}/{db_db}"
+
+    if db_uri is not None:
+        app.config["SQLALCHEMY_DATABASE_URI"] = db_uri
+    else:
+        app.logger.warn("Warning: No DB conection string set")
+
+
+def assemble_broker_uri_from_parts(app):
+    """Assemble the broker URI from its parts."""
+    rabbitmq_user = _get_config(app, "RABBITMQ_USER")
+    rabbitmq_password = _get_config(app, "RABBITMQ_PASSWORD")
+    broker_url = _get_config(app, "BROKER_URL")
+    broker_host = _get_config(app, "BROKER_HOST", default="localhost")
+    broker_protocol = _get_config(app, "BROKER_PROTOCOL", default="amqp")
+    broker_user = _get_config(app, "BROKER_USER", default=rabbitmq_user)
+    broker_password = _get_config(app, "BROKER_PASSWORD", default=rabbitmq_password)
+
+    if broker_url is None:
+        if None not in [broker_protocol, broker_user, broker_password, broker_host]:
+            broker_url = (
+                f"{broker_protocol}://{broker_user}:{broker_password}@{broker_host}/"
+            )
+    else:
+        broker_url = "amqp://guest:guest@localhost:5672/"
+
+    app.config.setdefault("BROKER_URL", broker_url)
+    app.config.setdefault("CELERY_BROKER_URL", broker_url)
+
+
+def assemble_cache_uri_from_parts(app):
+    """Assemble the various cache URIs from their parts."""
+    redis_user = _get_config(app, "CACHE_REDIS_USER")
+    redis_password = _get_config(app, "CACHE_REDIS_PASSWORD")
+    redis_host = _get_config(app, "CACHE_REDIS_HOST", default="localhost")
+    redis_port = _get_config(app, "CACHE_REDIS_PORT", default="6379")
+    redis_protocol = _get_config(app, "CACHE_REDIS_PROTOCOL", default="redis")
+    redis_db = _get_config(app, "CACHE_REDIS_DB", default="0")
+
+    # set the redis database names that should be used for various parts
+    account_sessions_db = _get_config(app, "ACCOUNTS_SESSION_REDIS_DB", default="1")
+    celery_results_db = _get_config(app, "CELERY_RESULT_BACKEND_DB", default="2")
+    ratelimit_storage_db = _get_config(app, "RATELIMIT_STORAGE_DB", default="3")
+    communities_identities_db = _get_config(
+        app, "COMMUNITIES_IDENTITIES_STORAGE_DB", default="4"
+    )
+
+    if redis_user is None and redis_password is not None:
+        # the default user in redis is named 'default'
+        redis_user = "default"
+
+    def _make_redis_url(db):
+        """Create redis URL from the given DB name."""
+        if redis_password is not None:
+            return f"{redis_protocol}://{redis_user}:{redis_password}@{redis_host}:{redis_port}/{db}"
+        else:
+            return f"{redis_protocol}://{redis_host}:{redis_port}/{db}"
+
+    cache_redis_url = _make_redis_url(redis_db)
+    accounts_session_redis_url = _make_redis_url(account_sessions_db)
+    celery_results_backend_url = _make_redis_url(celery_results_db)
+    ratelimit_storage_url = _make_redis_url(ratelimit_storage_db)
+    communities_identities_cache_url = _make_redis_url(communities_identities_db)
+
+    app.config.setdefault("CACHE_TYPE", "redis")
+    app.config.setdefault("CACHE_REDIS_URL", cache_redis_url)
+    app.config.setdefault("IIIF_CACHE_REDIS_URL", cache_redis_url)
+    app.config.setdefault("ACCOUNTS_SESSION_REDIS_URL", accounts_session_redis_url)
+    app.config.setdefault("CELERY_RESULT_BACKEND", celery_results_backend_url)
+    app.config.setdefault("RATELIMIT_STORAGE_URL", ratelimit_storage_url)
+    app.config.setdefault(
+        "COMMUNITIES_IDENTITIES_CACHE_REDIS_URL", communities_identities_cache_url
+    )
+
+
+def assemble_site_urls_from_parts(app):
+    """Create `LocalProxy` objects for the `SITE_{API,UI}_URL` items."""
+    # prefer the `SERVER_NAME` config item to build URLs
+    server_name = _get_config(app, "SERVER_NAME")
+    theme_siteurl = _get_config(
+        app, "THEME_SITEURL", default=f"https://{server_name or 'localhost'}"
+    )
+    if server_name:
+        hostname = server_name
+    else:
+        hostname = theme_siteurl.lstrip("http://").lstrip("https://").split("/")[0]
+
+    # note: 'invenio-cli run' likes to populate INVENIO_SITE_{UI,API}_URL...
+    app.config["SITE_UI_URL"] = LocalProxy(partial(_make_site_url, ""))
+    app.config["SITE_API_URL"] = LocalProxy(partial(_make_site_url, "/api"))
+    app.config.setdefault("OAISERVER_ID_PREFIX", hostname)
+
+
+def assemble_keycloak_config_from_parts(app):
+    """Assemble the Keycloak remote app from its parts."""
+    consumer_key = app.config.get("OAUTHCLIENT_KEYCLOAK_CONSUMER_KEY")
+    consumer_secret = app.config.get("OAUTHCLIENT_KEYCLOAK_CONSUMER_SECRET")
+
+    if consumer_key is not None and consumer_secret is not None:
+        app_credentials = {
+            "consumer_key": consumer_key,
+            "consumer_secret": consumer_secret,
+        }
+        app.config.setdefault("OAUTHCLIENT_KEYCLOAK_APP_CREDENTIALS", app_credentials)
+        app.config.setdefault("KEYCLOAK_APP_CREDENTIALS", app_credentials)
+
+    base_url = app.config.get("OAUTHCLIENT_KEYCLOAK_BASE_URL")
+    realm = app.config.get("OAUTHCLIENT_KEYCLOAK_REALM")
+    app_title = app.config.get("OAUTHCLIENT_KEYCLOAK_APP_TITLE")
+    app_description = app.config.get("OAUTHCLIENT_KEYCLOAK_APP_DESCRIPTION")
+
+    if base_url is not None and realm is not None:
+        helper = TUWSSOSettingsHelper(
+            title=app_title or "TU Wien SSO",
+            description=app_description or "TU Wien Single Sign-On",
+            base_url=base_url,
+            realm=realm,
+        )
+        remote_app = helper.remote_app
+
+        if app_title is not None:
+            remote_app["title"] = app_title
+
+        # ensure that this remote app is listed as "keycloak" in the app config
+        remote_apps = {
+            **(app.config.get("OAUTHCLIENT_REMOTE_APPS") or {}),
+            "keycloak": remote_app,
+        }
+        app.config.setdefault("OAUTHCLIENT_KEYCLOAK_REALM_URL", helper.realm_url)
+        app.config.setdefault(
+            "OAUTHCLIENT_KEYCLOAK_USER_INFO_URL", helper.user_info_url
+        )
+        app.config["OAUTHCLIENT_REMOTE_APPS"] = remote_apps
+
+
+def populate_unset_salt_values(app):
+    """Populate the salt values if they're not set yet."""
+    secret_key = app.config.get("SECRET_KEY", None)
+    if secret_key is None:
+        raise RuntimeError("SECRET_KEY configuration is unset! Aborting.")
+
+    app.config.setdefault("CSRF_SECRET_SALT", secret_key)
+    app.config.setdefault("SECURITY_RESET_SALT", secret_key)
+    app.config.setdefault("SECURITY_LOGIN_SALT", secret_key)
+    app.config.setdefault("SECURITY_PASSWORD_SALT", secret_key)
+    app.config.setdefault("SECURITY_CONFIRM_SALT", secret_key)
+    app.config.setdefault("SECURITY_CHANGE_SALT", secret_key)
+    app.config.setdefault("SECURITY_REMEMBER_SALT", secret_key)
+
+
+def assemble_and_populate_config(app):
+    """Assemble some config from their parts and populate some unset config."""
+    assemble_db_uri_from_parts(app)
+    assemble_broker_uri_from_parts(app)
+    assemble_cache_uri_from_parts(app)
+    assemble_site_urls_from_parts(app)
+    assemble_keycloak_config_from_parts(app)
+    populate_unset_salt_values(app)
+
+
+def override_prefixed_config(app):
+    """Override config items with their prefixed siblings' values.
+
+    The prefix is determined via the config item "CONFIG_TUW_CONFIG_OVERRIDE_PREFIX".
+    Configuration items with this prefix will override the values for
+
+    If the prefix is set to `None` (the default), then this feature will be disabled.
+    """
+    prefix = app.config.get("CONFIG_TUW_CONFIG_OVERRIDE_PREFIX", None)
+    if prefix is None:
+        return
+
+    prefix_len = len(prefix)
+    pairs = [(k, v) for k, v in app.config.items() if k.startswith(prefix)]
+
+    for key, value in pairs:
+        key = key[prefix_len:]
+        app.config[key] = value
+
+
+def override_flask_config(app):
+    """Replace the app's config with our own override.
+
+    This evaluates the ``LocalProxy`` objects used for ``SITE_{API,UI}_URL`` by
+    casting them into strings (which is their expected type).
+    """
+    app.config = TUWConfig.from_flask_config(app.config)
+
+    # we need to override the "config" global, as that might still be the
+    # old "normal" Flask config, and thus have different content
+    app.add_template_global(app.config, "config")
+
+
+def finalize_config(app):
+    """Finalize the application configuration with a few tweaks.
+
+    First, override the Flask configuration with the ``TUWConfig`` class.
+    Then, use prefixed configuration items to override non-prefixed ones
+    if a prefix is configured.
+    Lastly, assemble some configuration items from their parts (like building
+    the DB connection string from its pieces), and populate some unset
+    values like salts.
+    """
+    override_flask_config(app)
+    override_prefixed_config(app)
+    assemble_and_populate_config(app)
+
+
+def patch_flask_create_url_adapter(app):
+    """Patch Flask's {host,subdomain}_matching with 3.1 behavior.
+
+    See: https://github.com/pallets/flask/pull/5634
+
+    This can be removed once we get Flask 3.1+ in.
+    """
+    flask_version = importlib_metadata.version("flask").split(".", 2)
+    major = int(flask_version[0])
+    minor = int(flask_version[1])
+    if (major == 3 and minor >= 1) or (major > 3):
+        app.logger.info(
+            f"Flask version is {flask_version} (>= 3.1). "
+            "Skipping monkey patching app.create_url_adapter()."
+        )
+        return
+
+    def create_url_adapter(self, request):
+        """Create a URL adapter for the given request.
+
+        This function is from Flask 3.1, which is licensed under the 3-clause BSD.
+        """
+        if request is not None:
+            subdomain = None
+            server_name = self.config["SERVER_NAME"]
+
+            if self.url_map.host_matching:
+                # Don't pass SERVER_NAME, otherwise it's used and the actual
+                # host is ignored, which breaks host matching.
+                server_name = None
+            elif not self.subdomain_matching:
+                # Werkzeug doesn't implement subdomain matching yet. Until then,
+                # disable it by forcing the current subdomain to the default, or
+                # the empty string.
+                subdomain = self.url_map.default_subdomain or ""
+
+            return self.url_map.bind_to_environ(
+                request.environ, server_name=server_name, subdomain=subdomain
+            )
+
+        # Need at least SERVER_NAME to match/build outside a request.
+        if self.config["SERVER_NAME"] is not None:
+            return self.url_map.bind(
+                self.config["SERVER_NAME"],
+                script_name=self.config["APPLICATION_ROOT"],
+                url_scheme=self.config["PREFERRED_URL_SCHEME"],
+            )
+
+        return None
+
+    app.create_url_adapter = create_url_adapter.__get__(app, type(app))
diff --git a/invenio_config_tuw/startup/misc.py b/invenio_config_tuw/startup/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..cec10ed137bebd04110d782401a0cc73586ac410
--- /dev/null
+++ b/invenio_config_tuw/startup/misc.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2022-2025 TU Wien.
+#
+# Invenio-Config-TUW is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+
+"""Invenio-Config-TUW hacks and overrides to be applied on application startup.
+
+This module provides a blueprint whose sole purpose is to execute some code exactly
+once during application startup (via ``bp.record_once()``).
+These functions will be executed after the Invenio modules' extensions have been
+initialized, and thus we can rely on them being already available.
+"""
+
+from logging import ERROR
+from logging.handlers import SMTPHandler
+
+from invenio_rdm_records.services.search_params import MyDraftsParam
+from invenio_requests.proxies import current_request_type_registry
+
+from ..curations import TUWCurationRequest
+from ..logs import DetailedFormatter
+
+
+def register_smtp_error_handler(app):
+    """Register email error handler to the application."""
+    handler_name = "invenio-config-tuw-smtp-error-handler"
+
+    # check reasons to skip handler registration
+    error_mail_disabled = app.config.get("CONFIG_TUW_DISABLE_ERROR_MAILS", False)
+    if app.debug or app.testing or error_mail_disabled:
+        # email error handling should occur only in production mode, if not disabled
+        return
+
+    elif any([handler.name == handler_name for handler in app.logger.handlers]):
+        # we don't want to register duplicate handlers
+        return
+
+    elif "invenio-mail" not in app.extensions:
+        app.logger.warning(
+            (
+                "The Invenio-Mail extension is not loaded! "
+                "Skipping registration of SMTP error handler."
+            )
+        )
+        return
+
+    # check if mail server and admin email(s) are present in the config
+    # if not raise a warning
+    if app.config.get("MAIL_SERVER") and app.config.get("MAIL_ADMIN"):
+        # configure auth
+        username = app.config.get("MAIL_USERNAME")
+        password = app.config.get("MAIL_PASSWORD")
+        auth = (username, password) if username and password else None
+
+        # configure TLS
+        secure = None
+        if app.config.get("MAIL_USE_TLS"):
+            secure = ()
+
+        # initialize SMTP Handler
+        mail_handler = SMTPHandler(
+            mailhost=(app.config["MAIL_SERVER"], app.config.get("MAIL_PORT", 25)),
+            fromaddr=app.config["SECURITY_EMAIL_SENDER"],
+            toaddrs=app.config["MAIL_ADMIN"],
+            subject=app.config["THEME_SITENAME"] + " - Failure",
+            credentials=auth,
+            secure=secure,
+        )
+        mail_handler.name = handler_name
+        mail_handler.setLevel(ERROR)
+        mail_handler.setFormatter(DetailedFormatter())
+
+        # attach to the application
+        app.logger.addHandler(mail_handler)
+
+    else:
+        app.logger.warning(
+            "Mail configuration missing: SMTP error handler not registered!"
+        )
+
+
+def override_search_drafts_options(app):
+    """Override the "search drafts" options to show all accessible drafts."""
+    # doing this via config is (currently) not possible, as the `search_drafts`
+    # property can't be overridden with a config item (unlike `search`, above it)
+    # cf. https://github.com/inveniosoftware/invenio-rdm-records/blob/maint-10.x/invenio_rdm_records/services/config.py#L327-L332
+    try:
+        service = app.extensions["invenio-rdm-records"].records_service
+        service.config.search_drafts.params_interpreters_cls.remove(MyDraftsParam)
+    except ValueError:
+        pass
+
+
+def register_menu_entries(app):
+    """Register the curation setting endpoint in Flask-Menu."""
+    menu = app.extensions["menu"].root()
+    menu.submenu("settings.curation").register(
+        "invenio_config_tuw_settings.curation_settings_view",
+        '<i class="file icon"></i> Curation',
+        order=1,
+    )
+
+
+def customize_curation_request_type(app):
+    """Override the rdm-curations request type with our own version."""
+    current_request_type_registry.register_type(TUWCurationRequest(), force=True)
diff --git a/pyproject.toml b/pyproject.toml
index 426f619d797fecf81d98347e63f05b8d328bca5f..6cb4e70e10ba41abb24c7c53577af804305eac5d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -81,16 +81,14 @@ invenio_config_tuw_mail_handler = "invenio_config_tuw.startup:register_smtp_erro
 invenio_config_tuw_search_drafts = "invenio_config_tuw.startup:override_search_drafts_options"
 invenio_config_tuw_curation_settings = "invenio_config_tuw.startup:register_menu_entries"
 invenio_config_tuw_curation_request = "invenio_config_tuw.startup:customize_curation_request_type"
-invenio_config_tuw_flask_config = "invenio_config_tuw.startup:override_flask_config"
-invenio_config_tuw_override_prefixed_config = "invenio_config_tuw.startup:override_prefixed_config"
+invenio_config_tuw_finalize_config = "invenio_config_tuw.startup:finalize_config"
 invenio_config_tuw_patch_flask = "invenio_config_tuw.startup:patch_flask_create_url_adapter"
 
 [project.entry-points."invenio_base.api_finalize_app"]
 invenio_config_tuw_mail_handler = "invenio_config_tuw.startup:register_smtp_error_handler"
 invenio_config_tuw_search_drafts = "invenio_config_tuw.startup:override_search_drafts_options"
 invenio_config_tuw_curation_request = "invenio_config_tuw.startup:customize_curation_request_type"
-invenio_config_tuw_flask_config = "invenio_config_tuw.startup:override_flask_config"
-invenio_config_tuw_override_prefixed_config = "invenio_config_tuw.startup:override_prefixed_config"
+invenio_config_tuw_finalize_config = "invenio_config_tuw.startup:finalize_config"
 invenio_config_tuw_patch_flask = "invenio_config_tuw.startup:patch_flask_create_url_adapter"
 
 [project.entry-points."invenio_i18n.translations"]