diff --git a/portal/api/agenda.py b/portal/api/agenda.py
index ea5ac56b8bce64dfde9ad7f6bd804b0f00edce40..d7717f61c8dd24d72fbc449fb5c7488663a22241 100644
--- a/portal/api/agenda.py
+++ b/portal/api/agenda.py
@@ -7,7 +7,7 @@ from ..model import Key
 
 @bp.get("/agendas/-/items")
 @require_valid(Key)
-def the_agenda_items():
+def the_agenda_items() -> list[dict[str, str]]:
     tasks = sorted(
         TaigaUserStoryProvider.get_the_one().fetch_user_stories(),
         key=lambda task: task["kanban_order"],
diff --git a/portal/api/auth.py b/portal/api/auth.py
index acf7b0d33774cb7f889bd63b778c1b5bd6d20e19..bdbde6ae3438f5ecaf46454730ca4775311852b8 100644
--- a/portal/api/auth.py
+++ b/portal/api/auth.py
@@ -1,19 +1,25 @@
 # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details)
 # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/
 import functools
+from typing import Any, Callable
 
-from flask import request, g
+from flask import g, request
 
 from portal import db
 
+from ..model import Key, User
+from ..problem_details import ProblemResponse, not_found, unauthorized
 from .blueprint import bp
-from ..model import User, Key
-from ..problem_details import not_found, unauthorized
 
 
 @bp.before_app_request
-def set_user_if_valid():
-    if not request.authorization or request.authorization.type != "basic":
+def set_user_if_valid() -> None:
+    if (
+        not request.authorization
+        or request.authorization.type != "basic"
+        or request.authorization.username is None
+        or request.authorization.password is None
+    ):
         g.user = None
         return
 
@@ -23,15 +29,19 @@ def set_user_if_valid():
 
 
 @bp.before_app_request
-def set_key_if_valid():
-    if not request.authorization or request.authorization.type != "bearer":
+def set_key_if_valid() -> None:
+    if (
+        not request.authorization
+        or request.authorization.type != "bearer"
+        or request.authorization.token is None
+    ):
         g.key = None
         return
 
     g.key = Key.get_by_secret_unless_expired(request.authorization.token)
 
 
-def require_valid(credential_type):
+def require_valid(credential_type: type[User | Key]) -> Callable:
     """
     Requires that valid credentials are provided before allowing access to a route.
     If no valid credentials are provided, abort and send a 401.
@@ -41,9 +51,9 @@ def require_valid(credential_type):
     - `@require_valid(Key)` to require a valid key
     """
 
-    def decorator(route):
+    def decorator(route: Callable) -> Callable:
         @functools.wraps(route)
-        def protected_route(**kwargs):
+        def protected_route(**kwargs: Any) -> Any:
             # Determine which credentials to use
             credentials = {User: g.user, Key: g.key}.get(credential_type)
 
@@ -60,14 +70,14 @@ def require_valid(credential_type):
 
 @bp.post("/keys")
 @require_valid(User)
-def post_key():
+def post_key() -> tuple[dict, int]:
     key, secret = Key.generate(g.user)
     return key.to_dict() | {"secret": secret}, 201
 
 
 @bp.delete("/keys/<string:uuid>")
 @require_valid(Key)
-def delete_key(uuid: str):
+def delete_key(uuid: str) -> tuple[str, int] | ProblemResponse:
     key_to_delete = Key.get_only(uuid)
 
     if not key_to_delete:
diff --git a/portal/api/rest.py b/portal/api/rest.py
index dc763fb6d27034c796fbd5f78dc38a2f271723b7..e4d89e9d52ea71a717c688288785694bcddd450a 100644
--- a/portal/api/rest.py
+++ b/portal/api/rest.py
@@ -1,11 +1,11 @@
 from flask import render_template
 
+from ..problem_details import ProblemResponse, not_found
 from .blueprint import bp
-from ..problem_details import not_found
 
 
 @bp.get("/<path:_>")
-def fallback_not_found(_):
+def fallback_not_found(_: str) -> ProblemResponse:
     """
     If you can't find a matching API endpoint, fallback to returning a 404.
 
@@ -16,5 +16,5 @@ def fallback_not_found(_):
 
 
 @bp.get("/")
-def index():
+def index() -> str:
     return render_template("redocly.html")
diff --git a/portal/api/version.py b/portal/api/version.py
index 8674f682293185ae7d97f8aba24d9d7b3575cfed..b12b5274c26b0f98101ac958cc331e2013965f70 100644
--- a/portal/api/version.py
+++ b/portal/api/version.py
@@ -1,12 +1,13 @@
 import json
 from datetime import datetime
 from pathlib import Path
+from typing import Any
 
 from .blueprint import bp
 
 
 @bp.get("/version")
-def get_version():
+def get_version() -> dict[str, Any]:
     portal_version_path = Path("./PORTAL_VERSION")
     if portal_version_path.is_file():
         with open("PORTAL_VERSION") as f:
diff --git a/portal/main.py b/portal/main.py
index 327b6d81adfa8c5bc5ca422e57ed30fb928dc8bf..86220faf1137f83724b8f2c27e9b9706d060bce7 100644
--- a/portal/main.py
+++ b/portal/main.py
@@ -1,11 +1,12 @@
-from flask import Blueprint, render_template, redirect, url_for
+from flask import Blueprint, redirect, render_template, url_for
+from werkzeug import Response
 
 bp = Blueprint("main", __name__)
 
 
 @bp.get("/", defaults={"_": ""})
 @bp.get("/<path:_>")
-def index(_):
+def index(_: str) -> str:
     """
     Serve the index page at /*, i.e. at any path below / and at / itself.
 
@@ -16,5 +17,5 @@ def index(_):
 
 
 @bp.get("/api/")
-def redirect_to_api():
+def redirect_to_api() -> Response:
     return redirect(url_for("api.index"))
diff --git a/portal/model/Retrievable.py b/portal/model/Retrievable.py
index d754852cd8660460136385e0e80180ec4abf2526..1b7c6495c41b7e1aace3d3628fa627b34ae3b56a 100644
--- a/portal/model/Retrievable.py
+++ b/portal/model/Retrievable.py
@@ -1,7 +1,8 @@
 from abc import abstractmethod
-from typing import List, Optional, Any, Self
+from typing import Any, List, Optional, Self
 
 from flask import g
+
 from portal import db
 
 
diff --git a/portal/model/UtcDateTime.py b/portal/model/UtcDateTime.py
index a7e4abb12b2f8e9b6a3177cb21373f640c3f314d..7597b66e1f76efe4ef14325b46a616b906fb35a5 100644
--- a/portal/model/UtcDateTime.py
+++ b/portal/model/UtcDateTime.py
@@ -28,15 +28,15 @@ class UtcDateTime(types.TypeDecorator):
         return types.String(32)
 
     def process_bind_param(
-        self, value: datetime, dialect: Dialect
-    ) -> Optional[str | datetime]:
+        self, value: Optional[datetime], dialect: Dialect
+    ) -> Optional[str]:
         if value is None:
             return None
 
         return value.astimezone(timezone.utc).isoformat()
 
     def process_result_value(
-        self, value: str | datetime, dialect: Dialect
+        self, value: Optional[str], dialect: Dialect
     ) -> Optional[datetime]:
         if value is None:
             return None
diff --git a/portal/model/Uuid.py b/portal/model/Uuid.py
index 5eb2a46668316c38a7c866e92e11d7269bae3c7a..44739944fb779e468400078a0e54b23a771f714d 100644
--- a/portal/model/Uuid.py
+++ b/portal/model/Uuid.py
@@ -26,7 +26,9 @@ class Uuid(types.TypeDecorator):
     def load_dialect_impl(self, dialect: Dialect) -> types.TypeEngine[Any]:
         return types.String(UUID_LENGTH)
 
-    def process_bind_param(self, value: str | UUID, dialect: Dialect) -> Optional[str]:
+    def process_bind_param(
+        self, value: Optional[str | UUID], dialect: Dialect
+    ) -> Optional[str]:
         if isinstance(value, str):
             # Manually create UUID from string, to raise an error if the string is malformed
             UUID(value)
diff --git a/portal/problem_details.py b/portal/problem_details.py
index a397f0dec1b824ae526b8653cbcbd728244d7f97..851786d7c343f967b383cf7b1eb805b079b78ddf 100644
--- a/portal/problem_details.py
+++ b/portal/problem_details.py
@@ -2,17 +2,22 @@
 Provide RFC 7807-compliant problem details responses for the API.
 """
 
-from typing import Optional
+from typing import Any, Optional
 
+ResponseBody = dict[str, Any]
+HttpStatusCode = int
+HttpHeaders = dict[str, str]
+ProblemResponse = tuple[ResponseBody, HttpStatusCode, HttpHeaders]
 
-def unauthorized():
+
+def unauthorized() -> ProblemResponse:
     """
     Return a 401 message and status code
     """
     return _problem_response(title="Unauthorized", status=401)
 
 
-def not_found():
+def not_found() -> ProblemResponse:
     """
     Return a 404 message and status code
     """
@@ -24,7 +29,7 @@ def _problem_response(
     status: int,
     type_uri: Optional[str] = None,
     extensions: Optional[dict] = None,
-) -> tuple[dict, int, dict]:
+) -> ProblemResponse:
     type_dict = {"type": type_uri} if type_uri is not None else {}
     title_dict = {"title": title}