diff --git a/requirements-swh.txt b/requirements-swh.txt
index 5d7e9735eab0fd27f28560cbca0ef72b2959b8a9..9ddd77f3580f7aaed2578ea7b8e1d88ce2ae2e28 100644
--- a/requirements-swh.txt
+++ b/requirements-swh.txt
@@ -1,11 +1,11 @@
 swh.auth[django] >= 0.6.7
-swh.core >= 3.7.0
+swh.core >= 4.0.0
 swh.counters >= 0.5.1
-swh.indexer >= 3.7.0
+swh.indexer >= 4.0.0
 swh.model >= 6.16.0
 swh.provenance >= 0.1.1
-swh.scheduler >= 2.7.0
+swh.scheduler >= 3.0.0
 swh.search >= 0.16.0
-swh.storage >= 2.7.0
-swh-vault >= 1.12.2
+swh.storage >= 3.0.0
+swh-vault >= 2.0.0
 swh.webhooks >= 0.1.1
diff --git a/requirements-test.txt b/requirements-test.txt
index c298da3e52c70768bb3b46e90c425e5a0f7956a7..f56824ac610837eaa6c13cacc100b3230be10acb 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -10,15 +10,14 @@ pytest-django
 pytest-mock
 pytest-postgresql
 requests-mock != 1.9.0, != 1.9.1
-swh.core[http] >= 3.0.0
+swh.core[http] >= 4.0.0
 swh.graph >= 5.1.1
 swh.loader.git >= 0.8.0
-swh-scheduler[testing] >= 2.7.0
-swh.storage >= 0.1.1
+swh-scheduler[testing] >= 3.0.0
+swh.storage >= 3.0.0
 types-beautifulsoup4
 types-cryptography
 types-docutils
-types-psycopg2
 types-Pygments
 types-pyyaml
-types-requests
\ No newline at end of file
+types-requests
diff --git a/requirements.txt b/requirements.txt
index 4c9dc98d8ea1189876643608c17bed7a6b6e0b9e..348f4a4f8942e5b2730809cbeff5f618361c3955 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,7 +19,7 @@ iso8601
 looseversion
 msgpack
 prometheus-client
-psycopg2
+psycopg
 pybadges >= 2.2.1
 pygments
 pymemcache
diff --git a/swh/web/mailmap/management/commands/sync_mailmaps.py b/swh/web/mailmap/management/commands/sync_mailmaps.py
index 18b5d5963b449273e953494927138ffac36b14c2..33d93bd4ab5c932d8d63ecbe4cdb77b8b59ee010 100644
--- a/swh/web/mailmap/management/commands/sync_mailmaps.py
+++ b/swh/web/mailmap/management/commands/sync_mailmaps.py
@@ -5,9 +5,9 @@
 
 from __future__ import annotations
 
-import psycopg2
-import psycopg2.extensions
-from psycopg2.extras import execute_values
+from typing import Any
+
+import psycopg
 
 from django.core.management.base import BaseCommand
 from django.db import transaction
@@ -20,14 +20,14 @@ from swh.web.mailmap.models import UserMailmap
 DISABLE_MAILMAPS_QUERY = """\
 UPDATE person
    SET displayname = NULL
-   FROM (VALUES %s) AS emails (email)
+   FROM (VALUES (%s)) AS emails (email)
    WHERE person.email = emails.email
 """
 
 REFRESH_MAILMAPS_QUERY = """\
 UPDATE person
    SET displayname = displaynames.displayname
-   FROM (VALUES %s) AS displaynames (email, displayname)
+   FROM (VALUES (%s, %s)) AS displaynames (email, displayname)
    WHERE
      person.email = displaynames.email
      AND person.displayname IS DISTINCT FROM displaynames.displayname
@@ -47,24 +47,22 @@ class Command(BaseCommand):
 
     def disable_mailmaps(
         self,
-        storage_db: psycopg2.extensions.connection,
+        storage_db: psycopg.Connection[Any],
         mailmaps: QuerySet[UserMailmap, UserMailmap],
     ):
         """Return the SQL to disable a set of mailmaps"""
 
-        execute_values(
-            storage_db.cursor(),
+        storage_db.cursor().executemany(
             DISABLE_MAILMAPS_QUERY,
             ((mailmap.from_email.encode("utf-8"),) for mailmap in mailmaps),
         )
 
     def refresh_mailmaps(
         self,
-        storage_db: psycopg2.extensions.connection,
+        storage_db: psycopg.Connection[Any],
         mailmaps: QuerySet[UserMailmap, UserMailmap],
     ):
-        execute_values(
-            storage_db.cursor(),
+        storage_db.cursor().executemany(
             REFRESH_MAILMAPS_QUERY,
             (
                 (
@@ -99,7 +97,7 @@ class Command(BaseCommand):
                 )
             )
 
-            with psycopg2.connect(options["storage_dbconn"]) as db:
+            with psycopg.connect(options["storage_dbconn"]) as db:
                 self.disable_mailmaps(db, to_disable.select_for_update())
                 self.refresh_mailmaps(db, to_refresh.select_for_update())
                 if not options["perform"]:
diff --git a/swh/web/mailmap/tests/test_mailmap.py b/swh/web/mailmap/tests/test_mailmap.py
index 2f737f5c75fe0d60f735996766c28906397a0b76..f59a0297f26ca1fcd7da310389d19ed53ffcf12e 100644
--- a/swh/web/mailmap/tests/test_mailmap.py
+++ b/swh/web/mailmap/tests/test_mailmap.py
@@ -6,8 +6,6 @@
 import datetime
 import json
 
-import psycopg2
-from psycopg2.extras import execute_values
 import pytest
 
 from swh.model.model import Person
@@ -385,28 +383,29 @@ MAILMAP_KNOWN_PEOPLE = tuple(
 
 
 def init_stub_storage_db(postgresql):
-    if not isinstance(postgresql, psycopg2.extensions.connection):
-        # wrap pytest-postgresql >= 4's psycopg3 connection into a psycopg2 one
-        postgresql = psycopg2.connect(postgresql.info.dsn)
-    cur = postgresql.cursor()
-    cur.execute(
-        """
-        CREATE TABLE person (
-          fullname bytea PRIMARY KEY,
-          name bytea,
-          email bytea,
-          displayname bytea
+    with postgresql.cursor() as cur:
+        cur.execute(
+            """
+            CREATE TABLE person (
+              fullname bytea PRIMARY KEY,
+              name bytea,
+              email bytea,
+              displayname bytea
+            )
+            """
         )
-        """
-    )
-    execute_values(
-        cur,
-        "INSERT INTO person (fullname, name, email) VALUES %s",
-        (p.to_dict() for p in MAILMAP_KNOWN_PEOPLE),
-        template="(%(fullname)s, %(name)s, %(email)s)",
-    )
-    cur.execute("CREATE INDEX ON person (email)")
-    postgresql.commit()
-    cur.close()
+        cur.executemany(
+            "INSERT INTO person (fullname, name, email) VALUES (%s, %s, %s)",
+            [
+                (
+                    d["fullname"],
+                    d["name"],
+                    d["email"],
+                )
+                for d in (p.to_dict() for p in MAILMAP_KNOWN_PEOPLE)
+            ],
+        )
+        cur.execute("CREATE INDEX ON person (email)")
+        postgresql.commit()
 
-    return postgresql.dsn
+    return postgresql.info.dsn
diff --git a/swh/web/settings/production.py b/swh/web/settings/production.py
index 0e8127af45618f4e42d94b02689714b693f86f3a..33e04c8269ba55556e4f4fad9ec23ce76c28ea11 100644
--- a/swh/web/settings/production.py
+++ b/swh/web/settings/production.py
@@ -57,16 +57,14 @@ REST_FRAMEWORK["NUM_PROXIES"] = 2
 db_conf = swh_web_config["production_db"]
 if db_conf.get("name", "").startswith("postgresql://"):
     # poor man's support for dsn connection string...
-    import psycopg2
-
-    with psycopg2.connect(db_conf.get("name")) as cnx:
-        dsn_dict = cnx.get_dsn_parameters()
-
-    db_conf["name"] = dsn_dict.get("dbname")
-    db_conf["host"] = dsn_dict.get("host")
-    db_conf["port"] = dsn_dict.get("port")
-    db_conf["user"] = dsn_dict.get("user")
-    db_conf["password"] = dsn_dict.get("password")
+    import psycopg
+
+    with psycopg.connect(db_conf.get("name")) as cnx:
+        db_conf["name"] = cnx.info.dbname
+        db_conf["host"] = cnx.info.host
+        db_conf["port"] = cnx.info.port
+        db_conf["user"] = cnx.info.user
+        db_conf["password"] = cnx.info.password
 
 
 # https://docs.djangoproject.com/en/1.10/ref/settings/#databases