diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index af59d8454..16e1fca22 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 4.1.0-rc4
+current_version = 4.1.0
 tag_name = {new_version}
 commit = True
 tag = True
diff --git a/aleph/logic/api_keys.py b/aleph/logic/api_keys.py
index 96f960f3f..a8b1c0576 100644
--- a/aleph/logic/api_keys.py
+++ b/aleph/logic/api_keys.py
@@ -129,7 +129,6 @@ def hash_plaintext_api_keys():
     for index, partition in enumerate(results.partitions()):
         for role in partition:
             role.api_key_digest = hash_api_key(role.api_key)
-            role.api_key = None
             db.session.add(role)
             log.info(f"Hashing API key: {role}")
         log.info(f"Comitting partition {index}")
diff --git a/aleph/model/role.py b/aleph/model/role.py
index 7d3cc8f74..fcdaa67a1 100644
--- a/aleph/model/role.py
+++ b/aleph/model/role.py
@@ -1,7 +1,7 @@
 import logging
 from datetime import datetime, timezone
 from normality import stringify
-from sqlalchemy import or_, not_, func
+from sqlalchemy import and_, or_, not_, func
 from itsdangerous import URLSafeTimedSerializer
 from werkzeug.security import generate_password_hash, check_password_hash
 
@@ -197,13 +197,18 @@ def by_email(cls, email):
 
     @classmethod
     def by_api_key(cls, api_key):
-        if api_key is None:
+        if api_key is None or not len(api_key.strip()):
             return None
 
         q = cls.all()
 
         digest = hash_api_key(api_key)
-        q = q.filter(cls.api_key_digest == digest)
+        q = q.filter(
+            and_(
+                cls.api_key_digest != None,  # noqa: E711
+                cls.api_key_digest == digest,
+            )
+        )
 
         utcnow = datetime.now(timezone.utc)
         # TODO: Exclude API keys without expiration date after deadline
diff --git a/aleph/tests/test_api_keys.py b/aleph/tests/test_api_keys.py
index 111b7f791..1f155c930 100644
--- a/aleph/tests/test_api_keys.py
+++ b/aleph/tests/test_api_keys.py
@@ -211,7 +211,12 @@ def test_hash_plaintext_api_keys(self):
         hash_plaintext_api_keys()
 
         db.session.refresh(user_1)
-        assert user_1.api_key is None
+
+        # Do not delete the plaintext API key to allow for version rollbacks.
+        # `api_key` column will be removed in the next version at which point all
+        # plaintext keys will be deleted.
+        assert user_1.api_key == "1234567890"
+
         assert user_1.api_key_digest == hash_api_key("1234567890")
 
         db.session.refresh(user_2)
diff --git a/aleph/tests/test_view_context.py b/aleph/tests/test_view_context.py
index 2edf9cdf0..fbdee72b5 100644
--- a/aleph/tests/test_view_context.py
+++ b/aleph/tests/test_view_context.py
@@ -75,6 +75,10 @@ def test_authz_header_api_key_invalid(self):
         res = self.client.get(f"/api/2/roles/{self.role.id}", headers=headers)
         assert res.status_code == 403
 
+        headers = {"Authorization": "ApiKey   "}
+        res = self.client.get(f"/api/2/roles/{self.role.id}", headers=headers)
+        assert res.status_code == 403
+
         headers = {"Authorization": ""}
         res = self.client.get(f"/api/2/roles/{self.role.id}", headers=headers)
         assert res.status_code == 403
@@ -83,6 +87,10 @@ def test_authz_header_api_key_invalid(self):
         res = self.client.get(f"/api/2/roles/{self.role.id}", headers=headers)
         assert res.status_code == 403
 
+        headers = {"Authorization": "   "}
+        res = self.client.get(f"/api/2/roles/{self.role.id}", headers=headers)
+        assert res.status_code == 403
+
     def test_authz_url_param_api_key(self):
         query_string = {"api_key": "1234567890"}
         res = self.client.get(f"/api/2/roles/{self.role.id}", query_string=query_string)
@@ -97,3 +105,7 @@ def test_authz_url_params_api_key_invalid(self):
         query_string = {"api_key": ""}
         res = self.client.get(f"/api/2/roles/{self.role.id}", query_string=query_string)
         assert res.status_code == 403
+
+        query_string = {"api_key": "   "}
+        res = self.client.get(f"/api/2/roles/{self.role.id}", query_string=query_string)
+        assert res.status_code == 403
diff --git a/aleph/worker.py b/aleph/worker.py
index da7b05157..6f4942b70 100644
--- a/aleph/worker.py
+++ b/aleph/worker.py
@@ -3,7 +3,6 @@
 import time
 import threading
 import functools
-import queue
 import copy
 from typing import Dict, Callable
 import sqlalchemy
@@ -116,7 +115,13 @@ def __init__(
         version=None,
         prefetch_count_mapping=defaultdict(lambda: 1),
     ):
-        super().__init__(queues, conn=conn, num_threads=num_threads, version=version)
+        super().__init__(
+            queues,
+            conn=conn,
+            num_threads=num_threads,
+            version=version,
+            prefetch_count_mapping=prefetch_count_mapping,
+        )
         self.often = get_rate_limit("often", unit=300, interval=1, limit=1)
         self.daily = get_rate_limit("daily", unit=3600, interval=24, limit=1)
         # special treatment for indexing jobs - indexing jobs need to be batched
@@ -125,7 +130,6 @@ def __init__(
         # run of all available indexing tasks
         self.indexing_batch_last_updated = 0.0
         self.indexing_batches = defaultdict(list)
-        self.local_queue = queue.Queue()
         self.prefetch_count_mapping = prefetch_count_mapping
 
     def on_signal(self, signal, _):
diff --git a/aleph/wsgi.py b/aleph/wsgi.py
index 96e9cea9c..fbcf869ac 100644
--- a/aleph/wsgi.py
+++ b/aleph/wsgi.py
@@ -1,3 +1,5 @@
+import logging
+
 from aleph.core import create_app
 from aleph.settings import SETTINGS
 from aleph import __version__ as aleph_version
@@ -5,6 +7,7 @@
 import sentry_sdk
 from sentry_sdk.integrations.flask import FlaskIntegration
 
+log = logging.getLogger(__name__)
 
 if SETTINGS.SENTRY_DSN:
     sentry_sdk.init(
@@ -17,4 +20,5 @@
         environment=SETTINGS.SENTRY_ENVIRONMENT,
         send_default_pii=False,
     )
+log.info("aleph.wsgi initialized Sentry SDK")
 app = create_app()
diff --git a/contrib/aleph-traefik-minio-keycloak/docker-compose.yml b/contrib/aleph-traefik-minio-keycloak/docker-compose.yml
index 1ddb16302..2c9be830c 100644
--- a/contrib/aleph-traefik-minio-keycloak/docker-compose.yml
+++ b/contrib/aleph-traefik-minio-keycloak/docker-compose.yml
@@ -54,7 +54,7 @@ services:
       - "traefik.enable=false"
 
   worker:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: aleph worker
     restart: on-failure
     links:
@@ -79,7 +79,7 @@ services:
       - "traefik.enable=false"
 
   shell:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: /bin/bash
     depends_on:
       - postgres
@@ -99,7 +99,7 @@ services:
       - "traefik.enable=false"
 
   api:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: gunicorn -w 6 -b 0.0.0.0:8000 --log-level debug --log-file - aleph.wsgi:app
     expose:
       - 8000
@@ -121,7 +121,7 @@ services:
       - "traefik.enable=false"
 
   ui:
-    image: ghcr.io/alephdata/aleph-ui-production:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph-ui-production:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     depends_on:
       - api
       - traefik
diff --git a/contrib/keycloak/docker-compose.dev-keycloak.yml b/contrib/keycloak/docker-compose.dev-keycloak.yml
index 23ef4605f..b7a48e692 100644
--- a/contrib/keycloak/docker-compose.dev-keycloak.yml
+++ b/contrib/keycloak/docker-compose.dev-keycloak.yml
@@ -16,7 +16,7 @@ services:
   elasticsearch:
     build:
       context: services/elasticsearch
-    image: ghcr.io/alephdata/aleph-elasticsearch:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph-elasticsearch:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     hostname: elasticsearch
     environment:
       - discovery.type=single-node
@@ -55,7 +55,7 @@ services:
   app:
     build:
       context: .
-    image: alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     hostname: aleph
     command: /bin/bash
     links:
@@ -83,7 +83,7 @@ services:
   api:
     build:
       context: .
-    image: alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: aleph run -h 0.0.0.0 -p 5000 --with-threads --reload --debugger
     ports:
       - "127.0.0.1:5000:5000"
@@ -117,7 +117,7 @@ services:
   ui:
     build:
       context: ui
-    image: alephdata/aleph-ui:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: alephdata/aleph-ui:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     links:
       - api
     command: npm run start
diff --git a/docker-compose.yml b/docker-compose.yml
index 810cab902..5cf69f746 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -46,7 +46,7 @@ services:
       - aleph.env
 
   worker:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: aleph worker
     restart: on-failure
     depends_on:
@@ -62,7 +62,7 @@ services:
       - aleph.env
 
   shell:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     command: /bin/bash
     depends_on:
       - postgres
@@ -80,7 +80,7 @@ services:
       - aleph.env
 
   api:
-    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     expose:
       - 8000
     depends_on:
@@ -97,7 +97,7 @@ services:
       - aleph.env
 
   ui:
-    image: ghcr.io/alephdata/aleph-ui-production:${ALEPH_TAG:-ALEPH_TAG:-4.1.0-rc4}
+    image: ghcr.io/alephdata/aleph-ui-production:${ALEPH_TAG:-ALEPH_TAG:-4.1.0}
     depends_on:
       - api
     ports:
diff --git a/helm/charts/aleph/Chart.yaml b/helm/charts/aleph/Chart.yaml
index df5a04952..c45047028 100644
--- a/helm/charts/aleph/Chart.yaml
+++ b/helm/charts/aleph/Chart.yaml
@@ -2,5 +2,5 @@ apiVersion: v2
 name: aleph
 description: Helm chart for Aleph
 type: application
-version: 4.1.0-rc4
-appVersion: 4.1.0-rc4
\ No newline at end of file
+version: 4.1.0
+appVersion: 4.1.0
\ No newline at end of file
diff --git a/helm/charts/aleph/README.md b/helm/charts/aleph/README.md
index 6a1ad52a5..7f929071c 100644
--- a/helm/charts/aleph/README.md
+++ b/helm/charts/aleph/README.md
@@ -11,7 +11,7 @@ Helm chart for Aleph
 | global.amazon | bool | `true` | Are we using AWS services like s3? |
 | global.google | bool | `false` | Are we using GCE services like storage, vision api? |
 | global.image.repository | string | `"alephdata/aleph"` | Aleph docker image repo |
-| global.image.tag | string | `"global.image.tag | string | `"4.1.0-rc4"` | Aleph docker image tag |
+| global.image.tag | string | `"global.image.tag | string | `"4.1.0"` | Aleph docker image tag |
 | global.image.tag | string | `"Always"` |  |
 | global.namingPrefix | string | `"aleph"` | Prefix for the names of k8s resources |
 
diff --git a/helm/charts/aleph/values.yaml b/helm/charts/aleph/values.yaml
index 6307f5242..0aa54a080 100644
--- a/helm/charts/aleph/values.yaml
+++ b/helm/charts/aleph/values.yaml
@@ -6,7 +6,7 @@ global:
 
   image:
     repository: ghcr.io/alephdata/aleph
-    tag: "4.1.0-rc4"
+    tag: "4.1.0"
     pullPolicy: Always
 
   commonEnv:
diff --git a/requirements.txt b/requirements.txt
index 8aa8c15c4..a0670e687 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@ followthemoney==3.5.9
 followthemoney-store[postgresql]==3.1.0
 followthemoney-compare==0.4.4
 fingerprints==1.2.3
-servicelayer[google,amazon]==1.23.3-rc7
+servicelayer[google,amazon]==1.24.0
 normality==2.5.0
 pantomime==0.6.1
 
diff --git a/setup.py b/setup.py
index 4ae83513a..8c3941e29 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 setup(
     name="aleph",
-    version="4.1.0-rc4",
+    version="4.1.0",
     description="Document sifting web frontend",
     classifiers=[
         "Intended Audience :: Developers",
diff --git a/ui/package.json b/ui/package.json
index 44af3e415..1f8a00a59 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,6 +1,6 @@
 {
   "name": "aleph-ui",
-  "version": "4.1.0-rc4",
+  "version": "4.1.0",
   "private": true,
   "dependencies": {
     "@alephdata/followthemoney": "^3.5.5",
diff --git a/ui/src/components/Entity/EntityViews.jsx b/ui/src/components/Entity/EntityViews.jsx
index 1cf9b6027..3cc1270bf 100644
--- a/ui/src/components/Entity/EntityViews.jsx
+++ b/ui/src/components/Entity/EntityViews.jsx
@@ -174,8 +174,9 @@ class EntityViews extends React.Component {
 
     // The view mode is the default mode. It is rendered for almost all document-like
     // entities (except folders) and renders an empty state if no viewer is available
-    // for a specific file type.
-    if (entity.schema.isA('Folder')) {
+    // for a specific file type. Email is special cased, because it inherits from Folder,
+    // but still has a normal view.
+    if (entity.schema.isA('Folder') && !entity.schema.isA('Email')) {
       return;
     }