diff --git a/.gitignore b/.gitignore
index 923c74121381a8d77989b2b92582383d8a0f0c3b..d6afba32a2763b78f099a6e2001f806c50277060 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ core
 /.run/_*
 .envrc.local
 terraform*.log
+/.gitlab-ci-local
diff --git a/EDITING_INSIGHTS.md b/EDITING_INSIGHTS.md
index 6b84ccb6a4f0b70e04832d856f4956ae11c4dea8..e0f4d221f32c83231f66152a110937f05aa40b53 100644
--- a/EDITING_INSIGHTS.md
+++ b/EDITING_INSIGHTS.md
@@ -4,21 +4,11 @@ You can edit insights via the admin interface that Django provides.
 
 ## Editing insights on Production (and Staging)
 
-All okuna deployments in the cloud are not open to the internet. Therefore, we need to do a little work in order to
-connect to it.
-
 ### Using reverse proxy
 
-https://0kuna-adm1n.holi.social/admin currently provides the admin interface of the branch. We should switch this to production later. And provide access to staging as well via another domain.
-
-## Setting up editing insights
-
-### reverse proxy
-
-The reverse proxy is set up manually on the hop host. The hop host also is created manually. It has a public IPv4 and costs around 10€/month when running 24/7, 4€/month when stopped
-in between usages. We need to decide how to provide this service.
+https://0kuna-adm1n.holi.social/production/admin provides the admin interface for production, https://0kuna-adm1n.holi.social/staging/admin for the staging/main branch.
 
-For now I have created a schedule on which the VM starts 
+## Protocol of setting up editing insights
 
 ### Creating Superuser in DB
 
@@ -44,4 +34,4 @@ And then I could execute (in the project directory):
 echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@myproject.com', 'password')" | python manage.py shell
 ```
 
-(from https://stackoverflow.com/questions/6244382/how-to-automate-createsuperuser-on-django)
\ No newline at end of file
+(from https://stackoverflow.com/questions/6244382/how-to-automate-createsuperuser-on-django)
diff --git a/comments/admin.py b/comments/admin.py
index 082964b80ea5e7bc05cf4119f1fedbf30dcd1623..14ca80b51dc1e0df6e042e31a0c6229bf03b7858 100644
--- a/comments/admin.py
+++ b/comments/admin.py
@@ -1,4 +1,7 @@
 from django.contrib import admin
+from django.contrib.contenttypes.models import ContentType
+from django.urls import reverse
+from django.utils.html import format_html
 from rangefilter.filters import DateRangeQuickSelectListFilterBuilder
 
 from .models import Comment
@@ -13,13 +16,27 @@ class PostCommentAdmin(admin.ModelAdmin):
         "text",
         "created",
         "commenter",
+        "link_to_commentable",
         "language_code",
         "count_replies",
         "count_reactions",
         "count_mentions",
         "count_links",
         "is_deleted",
-        "commentable",
     )
     list_filter = (("created", DateRangeQuickSelectListFilterBuilder(title="Created")),)
     search_fields = ("text", "commenter")
+    ordering = ("-created",)
+
+    def link_to_commentable(self, obj):
+        content_type = ContentType.objects.get_for_model(model=obj.commentable_type.model_class())
+        link = reverse("admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(obj.commentable_id,))
+        commentable_content_type = ContentType.objects.get_for_model(model=obj.commentable_type.model_class())
+        commentable = commentable_content_type.get_object_for_this_type(pk=obj.commentable_id)
+        return format_html(
+            '<a href="{}">{}</a>',
+            link,
+            commentable,
+        )
+
+    link_to_commentable.short_description = "Commentable"  # new
diff --git a/okuna-cli.py b/okuna-cli.py
index 3afb139adc326c3a7f83313a885d8908a94ee604..9ca7322134a00444c9279ff04ed48ba1716ff4fc 100755
--- a/okuna-cli.py
+++ b/okuna-cli.py
@@ -57,7 +57,7 @@ def _remove_file_silently(filename):
 
 def _generate_random_string(
     length=12,
-    allowed_chars="abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
+    allowed_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
 ):
     """
     Return a securely generated random string.
diff --git a/openbook_auth/tasks.py b/openbook_auth/tasks.py
index 92918cd1fefc152a9b9620a389e4c26d20416c42..6e0c3c77dcd163e0e27c0dda03ad469e96cf6751 100644
--- a/openbook_auth/tasks.py
+++ b/openbook_auth/tasks.py
@@ -53,6 +53,7 @@ def update_ory_user_identity(user: User) -> bool:
 
 
 def update_ory(user_id, data):
+    # TODO: handle requests.exceptions.Timeout
     response = requests.patch(
         f"{settings.ORY_BASE_URL}/admin/identities/{user_id}", headers=ORY_HEADERS, json=data, timeout=30
     )
diff --git a/openbook_common/management/commands/worker_health_check.py b/openbook_common/management/commands/worker_health_check.py
index 4261a19dbe175796406ffc1c5b6e980fe5f35852..7c86f8c9df67e1643ac494881f56fe5cda9353cd 100644
--- a/openbook_common/management/commands/worker_health_check.py
+++ b/openbook_common/management/commands/worker_health_check.py
@@ -30,7 +30,7 @@ class Command(BaseCommand):
 
             if active_job_count >= ACTIVE_JOB_THRESHOLD:
                 send_alert_to_channel(
-                    f"*UH-OH: we have way too many active jobs " f"in {env}:{queue} right now: {active_job_count}!!*"
+                    f"*UH-OH: we have way too many active jobs in {env}:{queue} right now: {active_job_count}!!*"
                 )
                 print(f"{queue} has too many jobs {active_job_count}")
                 self.retval += 1
@@ -39,7 +39,7 @@ class Command(BaseCommand):
 
             if active_worker_count >= ACTIVE_WORKER_THRESHOLD:
                 send_alert_to_channel(
-                    f"*Hmm, we are not supposed to have " f"{active_worker_count} workers in " f"{env}:{queue}*"
+                    f"*Hmm, we are not supposed to have {active_worker_count} workers in {env}:{queue}*"
                 )
                 print(f"{queue} has too many workers {active_worker_count}")
 
diff --git a/openbook_common/serializers.py b/openbook_common/serializers.py
index 6475eb6d314c5627b2cd98833e403939bf572bb9..f6311946a41914a55fcaa5822166eef0fb03d646 100644
--- a/openbook_common/serializers.py
+++ b/openbook_common/serializers.py
@@ -339,7 +339,7 @@ class GeoFeatureModelSerializer(serializers.ModelSerializer):
         meta.auto_bbox = getattr(meta, "auto_bbox", False)
         if meta.bbox_geo_field and meta.auto_bbox:
             raise ImproperlyConfigured(
-                "You must eiher define a 'bbox_geo_field' or " "'auto_bbox', but you can not set both"
+                "You must either define a 'bbox_geo_field' or 'auto_bbox', but you can not set both"
             )
 
     def to_representation(self, instance):
diff --git a/openbook_common/tests/helpers.py b/openbook_common/tests/helpers.py
index 426fdef80930649534b55ae1ecaee1004c5ae2d3..a8c076aa91a699a0905ab457666c268009fafeec 100644
--- a/openbook_common/tests/helpers.py
+++ b/openbook_common/tests/helpers.py
@@ -92,7 +92,7 @@ def make_user(
     defined_arguments["id"] = defined_arguments["username"]
 
     user = mixer.blend(User, **defined_arguments)
-    profile = make_profile(user, **defined_arguments)
+    profile = make_profile(user, name=name, last_name=last_name, engagement_level=engagement_level)
     if user.profile.full_name() and not user.identity:
         user.identity = User.sanitize_identity_input(user.profile.full_name())
         user.save()
@@ -121,7 +121,7 @@ def make_profile(
     user: Optional[User] = None,
     name: Optional[str] = None,
     last_name: Optional[str] = None,
-    **kwargs,
+    engagement_level=None,
 ) -> UserProfile:
     defined_arguments = {k: v for k, v in locals().items() if v is not None}
     return mixer.blend(UserProfile, **defined_arguments)
diff --git a/openbook_communities/admin.py b/openbook_communities/admin.py
index 32d7ac224164f784e44eab3ebf9a08c7d7fc1740..3a7e8943f636f2d1d50f8f2dd4754a24ef8b240c 100644
--- a/openbook_communities/admin.py
+++ b/openbook_communities/admin.py
@@ -94,7 +94,7 @@ class CommunityAdmin(admin.ModelAdmin):
 
     def formfield_for_foreignkey(self, db_field, request, **kwargs):
         def nice_name(obj):
-            return f'{obj.profile.name if hasattr(obj, "profile") else ""} {obj.profile.last_name if hasattr(obj, "profile") else ""} ({obj.email}), {obj.username}'
+            return f"{obj.profile.name if hasattr(obj, 'profile') else ''} {obj.profile.last_name if hasattr(obj, 'profile') else ''} ({obj.email}), {obj.username}"
 
         formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
         if db_field.name == "creator":
diff --git a/openbook_communities/helpers.py b/openbook_communities/helpers.py
index 8e413d1ba20f1dc442b20d8866be3638b4199fe6..a1c56542b58980c5d7aeacee1c709127026dc5a5 100644
--- a/openbook_communities/helpers.py
+++ b/openbook_communities/helpers.py
@@ -59,9 +59,7 @@ def create_community_name_from_title_and_hash(title, hash):
     except ValidationError:
         name_suffix_length = min(8, len(hash))
         while name_suffix_length <= len(hash) and name_suffix_length > 0:
-            name = (
-                f"{slug[: settings.COMMUNITY_NAME_MAX_LENGTH - name_suffix_length - 1]}-{hash[: name_suffix_length]}"
-            )
+            name = f"{slug[: settings.COMMUNITY_NAME_MAX_LENGTH - name_suffix_length - 1]}-{hash[:name_suffix_length]}"
             try:
                 community_name_not_taken_validator(name)
                 return name
diff --git a/openbook_communities/tests/test_graphql_external_links.py b/openbook_communities/tests/test_graphql_external_links.py
index 9755034a25882b3eaedbc217510f18f12e7334fc..87c2b1316ad3e8fda8ba765c23716cac940f72b6 100644
--- a/openbook_communities/tests/test_graphql_external_links.py
+++ b/openbook_communities/tests/test_graphql_external_links.py
@@ -120,9 +120,9 @@ class TestExternalLinks:
 
             if "expected_code" in testCase:
                 field_errors = json.loads(response.errors[0].extensions["errors"])
-                assert (
-                    testCase["expected_code"] == field_errors[testCase["field"]][0]["code"]
-                ), f"{str(response.errors[0].extensions)} does not contain code: {testCase['expected_code']} from testCase: {testCase}"
+                assert testCase["expected_code"] == field_errors[testCase["field"]][0]["code"], (
+                    f"{str(response.errors[0].extensions)} does not contain code: {testCase['expected_code']} from testCase: {testCase}"
+                )
             else:
                 assert testCase["expected_error"] in str(response.errors[0].message)
             assert (response.data["addExternalLink"] if response.data else response.data) is None
@@ -346,9 +346,9 @@ class TestExternalLinks:
 
             if "expected_code" in testCase:
                 field_errors = json.loads(response.errors[0].extensions["errors"])
-                assert (
-                    testCase["expected_code"] == field_errors[testCase["field"]][0]["code"]
-                ), f"{str(response.errors[0].extensions)} does not contain code: {testCase['expected_code']} from testCase: {testCase}"
+                assert testCase["expected_code"] == field_errors[testCase["field"]][0]["code"], (
+                    f"{str(response.errors[0].extensions)} does not contain code: {testCase['expected_code']} from testCase: {testCase}"
+                )
             else:
                 assert testCase["expected_error"] in str(response.errors[0].message)
             assert (response.data["updateExternalLink"] if response.data else response.data) is None
diff --git a/openbook_invitations/management/commands/reset_invite_email_boolean.py b/openbook_invitations/management/commands/reset_invite_email_boolean.py
index 7c7afb69c2dfbcb9e108d50ad0affb5dfed65125..6f1acbd4f90727f3b61c85e22743f7e812234a30 100644
--- a/openbook_invitations/management/commands/reset_invite_email_boolean.py
+++ b/openbook_invitations/management/commands/reset_invite_email_boolean.py
@@ -15,7 +15,7 @@ class Command(BaseCommand):
         parser.add_argument(
             "--days",
             type=str,
-            help="Invites going back these many days will have their boolean" " reset if no user is created",
+            help="Invites going back these many days will have their boolean reset if no user is created",
         )
 
     def handle(self, *args, **options):
diff --git a/openbook_notifications/payloads.py b/openbook_notifications/payloads.py
index 528969075a1de12deda1818adffb9ee37e377dfd..a1ee4c355439a6c6327839ee397480351b0e15ff 100644
--- a/openbook_notifications/payloads.py
+++ b/openbook_notifications/payloads.py
@@ -619,7 +619,7 @@ class WelcomeEmailSeriesPayload(Payload):
                 "user": UserData.from_user(user).to_dict(),
                 "recommendedSpaces": [SpaceData.from_community(space).to_dict() for space in spaces],
                 "recommendedInsights": [InsightData.from_insight(insight).to_dict() for insight in insights],
-                "token": token,
+                "unsubscribeUrl": f"{app_url}/unsubscribe?series=welcome&email={user.email}&token={token}",
             },
             notification_type=NotificationType.WELCOME_EMAIL_SERIES.value,
             notification_version=WelcomeEmailSeriesPayload.NOTIFICATION_VERSION,
diff --git a/requirements-cli-only.txt b/requirements-cli-only.txt
index 3a4a4b8b424c850726b8c88f9ed9fc0ff26f2e7b..fa1eb6a468247bb3574261e54051f3cf306b22a0 100644
--- a/requirements-cli-only.txt
+++ b/requirements-cli-only.txt
@@ -1,5 +1,4 @@
 -i https://pypi.python.org/simple
-click==8.1.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-requests==2.28.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
-halo==0.0.31
-colorlog==6.7.0
+click>=8.1.8
+halo>=0.0.31
+colorlog>=6.9.0
diff --git a/requirements.txt b/requirements.txt
index 100c827dd8a23265fbc0d75960dc0f0e38f43c64..e1c992e88eb55e5c29eb23f0b14c3fed285906b6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,144 +1,140 @@
 # For upgrading all dependencies to their highest compatible versions:
 #
-# sed -i 's/[~=]=/>=/' requirements.txt          # replace all version specifiers with a companion specifier
-# pip install -U -r requirements.txt                # now upgrade all packages in requirements.txt
-# pip freeze | sed 's/==/~=/' > requirements.txt    # freeze adds any transient dependencies and makes them explicit
-#                                                   # then, we transform the exact matches to compatibility matches
+# sed -i 's/[~=]=/>=/' requirements.txt   # replace all version specifiers with a companion specifier
+# pip install -U -r requirements.txt      # now upgrade all packages in requirements.txt
+# pip freeze                              # freeze adds any transient dependencies and makes them explicit
 #
 # Afterwards, some packages might need a downgrade or version tweak for the bundle fitting together and fitting the code
-adrf~=0.1.8
-aiofiles~=24.1.0
-aiohappyeyeballs~=2.4.3
-aiohttp~=3.11.11
-aiosignal~=1.3.1
-ASGIMiddlewareStaticFile~=0.6.1
-asgiref~=3.8.1
-async-property~=0.2.2
-attrs~=24.3.0
-backoff~=2.2.1
-beautifulsoup4~=4.13.0
-black~=24.10.0
-blurhash-python~=1.2.2
-cachetools~=5.5.0
-certifi~=2024.12.14
-cffi~=1.17.1
-charset-normalizer~=3.4.0
-click~=8.1.7
-colorama~=0.4.6
-colorlog~=6.9.0
-coverage~=7.6.4
-Deprecated~=1.2.14
-Django~=5.0.7
-django-admin-rangefilter~=0.13.2
-django-appconf~=1.0.6
-django-cacheops~=7.1.0
-django-cors-headers~=4.6.0
-django-cursor-pagination~=0.3.0
-django-debug-toolbar~=4.4.6
-django-extensions~=3.2.3
-django-imagekit~=5.0.0
-django-ipware~=7.0.1
-django-modeltranslation~=0.19.10
-django-ordered-model~=3.7.4
-django-proxy~=1.3.0
-django-redis~=5.4.0
-django-rq~=3.0.0
-django-sortedm2m~=4.0.0
-django-structlog~=8.1.0
-djangorestframework~=3.15.2
-djangorestframework-camel-case~=1.4.2
-execnet~=2.1.1
-Faker~=12.0.1 # mixer 7.2.2 depends on Faker<12.1 and >=5.4.0
-filelock~=3.17.0
-frozenlist~=1.5.0
-funcy~=2.0
-google-api-core~=2.24.1
-google-auth~=2.38.0
-google-cloud-pubsub~=2.28.0
-google-cloud-webrisk~=1.16.0
-googleapis-common-protos~=1.66.0
-graphql-core~=3.2.5
-grpc-google-iam-v1~=0.13.1
-grpcio~=1.70.0
-grpcio-status~=1.70.0
-h11~=0.14.0
-halo~=0.0.31
-hiredis~=3.1.0
-icalendar~=6.1.1
-idna~=3.10
+adrf==0.1.9
+aiofiles==24.1.0
+aiohappyeyeballs==2.4.6
+aiohttp==3.11.13
+aiosignal==1.3.2
+ASGIMiddlewareStaticFile==0.6.1
+asgiref==3.8.1
+async-property==0.2.2
+attrs==25.1.0
+backoff==2.2.1
+beautifulsoup4==4.13.3
+black==25.1.0
+blurhash-python==1.2.2
+cachetools==5.5.2
+certifi==2025.1.31
+cffi==1.17.1
+charset-normalizer==3.4.1
+colorama==0.4.6
+coverage==7.6.12
+Deprecated==1.2.18
+Django==5.0.12
+django-admin-rangefilter==0.13.2
+django-appconf==1.1.0
+django-cacheops==7.1
+django-cors-headers==4.7.0
+django-cursor-pagination==0.3.0
+django-debug-toolbar==5.0.1
+django-extensions==3.2.3
+django-imagekit==5.0.0
+django-ipware==7.0.1
+django-modeltranslation==0.19.12
+django-ordered-model==3.7.4
+django-proxy==1.3.0
+django-redis==5.4.0
+django-rq==3.0.0
+django-sortedm2m==4.0.0
+django-structlog==9.0.1
+djangorestframework==3.15.2
+djangorestframework-camel-case==1.4.2
+execnet==2.1.1
+Faker==12.0.1 # mixer 7.2.2 depends on Faker<12.1 and >=5.4.0
+filelock==3.17.0
+frozenlist==1.5.0
+funcy==2.0
+google-api-core==2.24.1
+google-auth==2.38.0
+google-cloud-pubsub==2.28.0
+google-cloud-webrisk==1.17.0
+googleapis-common-protos==1.68.0
+graphql-core==3.2.6
+grpc-google-iam-v1==0.14.0
+grpcio==1.70.0
+grpcio-status==1.70.0
+h11==0.14.0
+hiredis==3.1.0
+icalendar==6.1.1
+idna==3.10
 imagekitio==2.2.8 # version 3 contains many breaking changes
-importlib_metadata~=8.4.0
-iniconfig~=2.0.0
-Jinja2~=3.1.4
-langdetect~=1.0.9
-log-symbols~=0.0.14
-MarkupSafe~=3.0.2
-mixer~=7.2.2
-monotonic~=1.6
-multidict~=6.1.0
-mypy-extensions~=1.0.0
-novu~=1.14.0
-opentelemetry-api~=1.27.0
-opentelemetry-sdk~=1.27.0
-opentelemetry-semantic-conventions~=0.48b0
-packaging~=24.1
-pathspec~=0.12.1
-pilkit~=3.0
-pillow~=11.1.0
-platformdirs~=4.3.6
-pluggy~=1.5.0
-posthog~=3.11.0
-propcache~=0.2.0
-proto-plus~=1.26.0
-protobuf~=5.29.3
-psycopg~=3.2.3
-psycopg-binary~=3.2.3
-pyasn1~=0.6.1
-pyasn1_modules~=0.4.1
-pycparser~=2.22
-PyJWT~=2.10.1
-pytest~=8.3.3
-pytest-asyncio~=0.24.0
-pytest-cov~=6.0.0
-pytest-django~=4.9.0
-pytest-xdist~=3.6.1
-python-benedict~=0.34.0
-python-dateutil~=2.9.0.post0
-python-dotenv~=1.0.1
-python-fsutil~=0.14.1
-python-ipware~=3.0.0
-python-magic~=0.4.27
-python-slugify~=8.0.4
-pytz~=2024.2
-redis~=5.2.0
-requests~=2.32.3
-requests-file~=2.1.0
-requests-toolbelt~=1.0.0
-rest-framework-generic-relations~=2.2.0
-rq~=2.1.0
-rsa~=4.9
-ruamel.yaml~=0.18.6
-ruamel.yaml.clib~=0.2.12
-ruff~=0.7.1
-sentry-sdk~=2.20.0
-six~=1.17.0
-soupsieve~=2.6
-spinners~=0.0.24
-sqlparse~=0.5.1
-strawberry-graphql~=0.247.0
-strawberry-graphql-django~=0.49.1
-structlog~=24.4.0
-termcolor~=2.5.0
-text-unidecode~=1.3
-tldextract~=5.1.2
-typing_extensions~=4.12.2
-tzdata~=2024.2
-Unidecode~=1.3.8
-uritools~=4.0.3
-url-normalize~=1.4.3
-urlextract~=1.9.0
-urllib3~=2.3.0
-uvicorn~=0.32.0
-wrapt~=1.16.0
-yarl~=1.17.1
-zipp~=3.20.2
+importlib_metadata==8.5.0
+iniconfig==2.0.0
+Jinja2==3.1.5
+langdetect==1.0.9
+log-symbols==0.0.14
+MarkupSafe==3.0.2
+mixer==7.2.2
+monotonic==1.6
+multidict==6.1.0
+mypy-extensions==1.0.0
+novu==1.14.0
+opentelemetry-api==1.30.0
+opentelemetry-sdk==1.30.0
+opentelemetry-semantic-conventions==0.51b0
+packaging==24.2
+pathspec==0.12.1
+pilkit==3.0
+pillow==11.1.0
+platformdirs==4.3.6
+pluggy==1.5.0
+posthog==3.15.1
+propcache==0.3.0
+proto-plus==1.26.0
+protobuf==5.29.3
+psycopg==3.2.5
+psycopg-binary==3.2.5
+pyasn1==0.6.1
+pyasn1_modules==0.4.1
+pycparser==2.22
+PyJWT==2.10.1
+pytest==8.3.4
+pytest-asyncio==0.25.3
+pytest-cov==6.0.0
+pytest-django==4.10.0
+pytest-xdist==3.6.1
+python-benedict==0.34.1
+python-dateutil==2.9.0.post0
+python-dotenv==1.0.1
+python-fsutil==0.15.0
+python-ipware==3.0.0
+python-magic==0.4.27
+python-slugify==8.0.4
+pytz==2025.1
+redis==5.2.1
+requests==2.32.3
+requests-file==2.1.0
+requests-toolbelt==1.0.0
+rest-framework-generic-relations==2.2.0
+rq==2.1.0
+rsa==4.9
+ruamel.yaml==0.18.10
+ruamel.yaml.clib==0.2.12
+ruff==0.9.7
+sentry-sdk==2.22.0
+six==1.17.0
+soupsieve==2.6
+spinners==0.0.24
+sqlparse==0.5.3
+strawberry-graphql==0.248.1
+strawberry-graphql-django==0.53.3
+structlog==25.1.0
+termcolor==2.5.0
+text-unidecode==1.3
+tldextract==5.1.3
+typing_extensions==4.12.2
+tzdata==2025.1
+Unidecode==1.3.8
+uritools==4.0.3
+url-normalize==1.4.3
+urlextract==1.9.0
+urllib3==2.3.0
+uvicorn==0.34.0
+wrapt==1.17.2
+yarl==1.18.3
+zipp==3.21.0
diff --git a/terraform/common/init.tf b/terraform/common/init.tf
index 103aab65f38f49bed1efa1b25acf50407663eda4..7830c48f1a18dcec171e6f9252b12b70b8bba9ab 100644
--- a/terraform/common/init.tf
+++ b/terraform/common/init.tf
@@ -1,4 +1,16 @@
 terraform {
+  # allow the lowest common version across all projects, so that the current CI docker image version suits all projects
+  required_version = ">= 1.9"
+  required_providers {
+    google = {
+      source  = "hashicorp/google"
+      version = "6.22.0"
+    }
+    google-beta = {
+      source  = "hashicorp/google-beta"
+      version = "6.21.0"
+    }
+  }
   backend "gcs" {
     bucket = "holi-shared-terraform-state"
     prefix = "okuna-common"
diff --git a/terraform/environments/deployment.tf b/terraform/environments/deployment.tf
index 1255610cf18d151f20fc01768175d4f5979a2796..7c9ff7ca9f906efcc49cd33a46b4fad587e3e7ec 100644
--- a/terraform/environments/deployment.tf
+++ b/terraform/environments/deployment.tf
@@ -421,6 +421,26 @@ resource "google_cloud_run_service" "okuna" {
             }
           }
         }
+
+        startup_probe {
+          initial_delay_seconds = 10
+          period_seconds        = 5
+          timeout_seconds       = 5
+          failure_threshold     = 10
+          http_get {
+            path = "/graphql?query=%7B__typename%7D"
+          }
+        }
+
+        liveness_probe {
+          period_seconds    = 15
+          timeout_seconds   = 10
+          failure_threshold = 3
+          http_get {
+            path = "/graphql?query=%7B__typename%7D"
+          }
+        }
+
         resources {
           limits = {
             # cpu can only be scaled down to 1000m as long as container_concurrency is set to != 1
diff --git a/terraform/environments/init.tf b/terraform/environments/init.tf
index 3bdf189408a60cc0aed2520e15c3f01200432ed4..98453d9f5244026a399ba0e1f20fab7ffd990190 100644
--- a/terraform/environments/init.tf
+++ b/terraform/environments/init.tf
@@ -1,4 +1,16 @@
 terraform {
+  # allow the lowest common version across all projects, so that the current CI docker image version suits all projects
+  required_version = ">= 1.9"
+  required_providers {
+    google = {
+      source  = "hashicorp/google"
+      version = "6.22.0"
+    }
+    google-beta = {
+      source  = "hashicorp/google-beta"
+      version = "6.21.0"
+    }
+  }
   backend "gcs" {
     bucket = "holi-shared-terraform-state"
     prefix = "okuna-environments"
diff --git a/terraform/environments/scripts/wait-for-ssl.sh b/terraform/environments/scripts/wait-for-ssl.sh
index e130aadf5d8efde49192249d4b826ffd64a61ef6..2a53fa605f62bcdb379948e214c8718565f6cc51 100755
--- a/terraform/environments/scripts/wait-for-ssl.sh
+++ b/terraform/environments/scripts/wait-for-ssl.sh
@@ -7,7 +7,7 @@ url="$1"
 
 # google has a cdn answering on requests. This cdn takes a while to be fully updated.
 # Therefore, we don't return on first success, but on a number of consecutive successes.
-number_of_consecutive_successful_tries_needed=10
+number_of_consecutive_successful_tries_needed=25
 number_of_consecutive_successful_tries_achieved=0
 
 [ -z "$url" ] && echo "missing url as first param" && exit 1
@@ -17,10 +17,21 @@ echo -n "Checking if SSL certificate for $url is installed: "
 # storage for the return value of the curl command
 retval=0
 
+# early break on success (no need to wait on existing deployments)
+set +e
+curl -sSLIm 10 "$url" > /dev/null 2>& 1 
+retval=$?
+set -e
+if [ $retval -eq 0 ]; then
+  echo "success on first try, not checking further"
+  exit 0
+fi
+
+
 # shellcheck disable=SC2034
 for i in {1..2500}; do
   set +e
-  out=$(curl -sSLI "$url" 2>&1)
+  out=$(curl -sSLIm 10 "$url" 2>& 1)
   retval=$?
   set -e
   # shellcheck disable=SC2181
@@ -31,6 +42,7 @@ for i in {1..2500}; do
       echo "test successful after $i total tries"
       exit 0
     fi
+    sleep 1
   else
     echo -n "."
     number_of_consecutive_successful_tries_achieved=0