diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 082c6e97ff31d90274e3aa7063c1bf2d840d36fb..286e825730b0cbd9f1f3072b64d73841dd168d06 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -43,7 +43,7 @@ build_docker:
 .deploy:
   stage: "deploy"
   image:
-    name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.11.0'
+    name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.11.2'
     # default entrypoint is terraform command, but we want to run shell scripts
     entrypoint: ["/bin/sh", "-c"]
   variables:
@@ -58,17 +58,18 @@ build_docker:
     expire_in: 1 week
   script:
     - terraform/environments/scripts/create-or-update-env.sh "$ENVIRONMENT_ID" "$CI_COMMIT_SHA"
-    - echo "$(terraform/scripts/get-output.sh api_domain)" > "$API_DOMAIN_PATH"
+    - echo "$(terraform/environments/scripts/get-output.sh api_domain)" > "$API_DOMAIN_PATH"
   resource_group: $ENVIRONMENT_ID # never execute terraform in parallel on the same environment
   interruptible: false
 
-.e2e:
+.smoketest:
   stage: "deploy"
-  image: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/archlinux:base'
+  image: 'europe-north1-docker.pkg.dev/holi-shared/docker/holi-docker/holi-k6-builder'
   script:
-    - API_BASE_URL=`cat "$API_DOMAIN_PATH"`
-    - echo "e2e tests against '$CI_ENVIRONMENT_SLUG' environment go here and against '$API_BASE_URL'"
-    - terraform/scripts/wait-for-ssl.sh "https://${API_BASE_URL}"
+    - API_DOMAIN=$(cat "$API_DOMAIN_PATH")
+    - terraform/environments/scripts/wait-for-ssl.sh "https://${API_DOMAIN}"
+    - BASE_URL="https://${API_DOMAIN}" k6 run smoketest/main.js
+    # TODO should/could we roll back the service to the last working revision on test failure?
 
 staging_deploy:
   extends: .deploy
@@ -81,6 +82,12 @@ staging_deploy:
   only:
     - main
 
+staging_smoketest:
+  extends: .smoketest
+  needs: ['staging_deploy']
+  only:
+    - main
+
 production_deploy:
   extends: .deploy
   allow_failure: false
@@ -92,3 +99,9 @@ production_deploy:
     ENVIRONMENT_ID: production
   only:
     - production
+
+production_smoketest:
+  extends: .smoketest
+  needs: ['production_deploy']
+  only:
+    - production
diff --git a/.terraform-version b/.terraform-version
index 1cac385c6cb864bab53f6846e112f5a93fd17401..ca7176690dd6f501842f3ef4b70bb32118edb489 100644
--- a/.terraform-version
+++ b/.terraform-version
@@ -1 +1 @@
-1.11.0
+1.11.2
diff --git a/smoketest/main.js b/smoketest/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..54305b9294967e204af5bdc931d5c3ec373bd68c
--- /dev/null
+++ b/smoketest/main.js
@@ -0,0 +1,40 @@
+import http from 'k6/http'
+import { check } from 'k6'
+
+// You don't need to change anything in this section, it's k6 glue code.
+// See the default function at the end of the file for defining your smoketest.
+// This configuration only executes 1 test, enough for a smoketest. The smoketest will fail on any check failing.
+const allChecksNeedToPassTreshold = { checks: [{ threshold: 'rate==1', abortOnFail: true }] }
+export const options = {
+  vus: 1,
+  iterations: 1,
+  thresholds: allChecksNeedToPassTreshold,
+}
+
+/**
+ * Performs a GraphQL query and checks the response using the provided function. Fails if any of the provided expectations are not met.
+ * @param {string} query The GraphQL query to perform
+ * @param {(response: http.Response) => Array<boolean>} checkFunction
+ *   A function that takes the HTTP response as an argument and returns an array
+ *   of boolean values, each indicating success or failure of a test.
+ */
+function forGetRequest(path, checkFunction) {
+  const response = http.get(`${__ENV.BASE_URL}${path}`, {
+    headers: { 'Accept': 'application/json' },
+  })
+  checkFunction(response)
+}
+
+// Define your smoketest(s) here.
+export default () => {
+  forGetRequest("/_matrix/client/r0/login", (response) => {
+    check(response, {
+      'is status 200': (r) => r.status === 200,
+    })
+    check(JSON.parse(response.body), {
+      // there can be multiple tests here, e.g.
+      //"contains topics object": (r) => typeof r.data.topics != null,
+      'contains at least one flow': (r) => r.flows && Array.isArray(r.flows) && r.flows.length > 0,
+    })
+  })
+}
diff --git a/terraform/common/init.tf b/terraform/common/init.tf
index cfe77da9ee716054b10dd1574a57c2b0b10dc6a2..72ebcdcdac39750e6ca22e626425112756673914 100644
--- a/terraform/common/init.tf
+++ b/terraform/common/init.tf
@@ -4,11 +4,11 @@ terraform {
   required_providers {
     google = {
       source  = "hashicorp/google"
-      version = "6.24.0"
+      version = "6.25.0"
     }
     google-beta = {
       source  = "hashicorp/google-beta"
-      version = "6.24.0"
+      version = "6.25.0"
     }
   }
   backend "gcs" {
diff --git a/terraform/environments/init.tf b/terraform/environments/init.tf
index 56fdd63cc92c6018df55593fe05a97e37781fa96..bfd00ff860ffd6745710e7475fd5e375ff6c79d4 100644
--- a/terraform/environments/init.tf
+++ b/terraform/environments/init.tf
@@ -4,11 +4,11 @@ terraform {
   required_providers {
     google = {
       source  = "hashicorp/google"
-      version = "6.24.0"
+      version = "6.25.0"
     }
     google-beta = {
       source  = "hashicorp/google-beta"
-      version = "6.24.0"
+      version = "6.25.0"
     }
   }
   backend "gcs" {