diff --git a/.ci/macos/Brewfile b/.ci/macos/Brewfile
new file mode 100644
index 0000000000000000000000000000000000000000..338713c04b50928037d043199d6c0cae29e7c03f
--- /dev/null
+++ b/.ci/macos/Brewfile
@@ -0,0 +1,7 @@
+tap "nlohmann/json"
+
+brew "pkg-config"
+brew "cmake"
+brew "ninja"
+brew "openssl"
+brew "nlohmann_json"
diff --git a/.ci/synapse/Dockerfile b/.ci/synapse/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a7692ddd87ad455e264a1c89423723e3719911d5
--- /dev/null
+++ b/.ci/synapse/Dockerfile
@@ -0,0 +1,12 @@
+FROM matrixdotorg/synapse:v1.24.0
+
+COPY setup-synapse.sh /setup-synapse.sh
+COPY entrypoint.sh /entrypoint.sh
+COPY service /service
+
+RUN /setup-synapse.sh
+
+ENTRYPOINT ["/entrypoint.sh"]
+
+EXPOSE 8008
+
diff --git a/.ci/synapse/entrypoint.sh b/.ci/synapse/entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d21a79ac6e807a164481321c29c53ad9ae7fdf61
--- /dev/null
+++ b/.ci/synapse/entrypoint.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+sv_stop() {
+    for s in $(ls -d /service/*)
+    do
+        sv stop $s
+    done
+}
+
+trap "sv_stop; exit" SIGTERM
+runsvdir /service &
+wait
diff --git a/.ci/synapse/service/postgresql/run b/.ci/synapse/service/postgresql/run
new file mode 100755
index 0000000000000000000000000000000000000000..6a35afa0a193bbd28e5a0603ce75eeb382d4eabd
--- /dev/null
+++ b/.ci/synapse/service/postgresql/run
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec chpst -u postgres:postgres /usr/lib/postgresql/11/bin/postgres -D '/data2/db' 2>&1
diff --git a/.ci/synapse/service/synapse/run b/.ci/synapse/service/synapse/run
new file mode 100755
index 0000000000000000000000000000000000000000..0c81e9dbd268892074bc556dd9bef6ddc169a0e9
--- /dev/null
+++ b/.ci/synapse/service/synapse/run
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec /start.py
diff --git a/.ci/synapse/setup-synapse.sh b/.ci/synapse/setup-synapse.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2cc2311b5cbec69db4ea7c29acae8606cbd5ad52
--- /dev/null
+++ b/.ci/synapse/setup-synapse.sh
@@ -0,0 +1,99 @@
+#!/bin/sh
+
+set -e
+
+export DEBIAN_FRONTEND=noninteractive
+
+apt-get update && apt-get -y install --no-install-recommends runit postgresql openssl
+
+
+mkdir /data2
+
+mkdir /data2/db
+chown postgres /data2/db
+
+# Initialise & start the database
+su -c '/usr/lib/postgresql/11/bin/initdb -D /data2/db -E "UTF-8" --lc-collate="C" --lc-ctype="C" --username=postgres' postgres
+su -c '/usr/lib/postgresql/11/bin/pg_ctl -w -D /data2/db start' postgres
+su -c '/usr/lib/postgresql/11/bin/createuser synapse_user' postgres
+su -c '/usr/lib/postgresql/11/bin/createdb -O synapse_user synapse' postgres
+
+sed -i 's,/data,/data2,g' /start.py
+sed -i 's,/data,/data2,g' /conf/homeserver.yaml
+
+SYNAPSE_SERVER_NAME=synapse SYNAPSE_REPORT_STATS=no /start.py generate
+
+perl -pi -w -e \
+    's/#enable_registration: false/enable_registration: true/g;' data2/homeserver.yaml
+perl -pi -w -e \
+    's/tls: false/tls: true/g;' data2/homeserver.yaml
+perl -pi -w -e \
+    's/#tls_certificate_path:/tls_certificate_path:/g;' data2/homeserver.yaml
+perl -pi -w -e \
+    's/#tls_private_key_path:/tls_private_key_path:/g;' data2/homeserver.yaml
+
+openssl req -x509 -newkey rsa:4096 -keyout data2/synapse.tls.key -out data2/synapse.tls.crt -days 365 -subj '/CN=synapse' -nodes
+chmod 0777 data2/synapse.tls.crt
+chmod 0777 data2/synapse.tls.key
+
+# set db config to postgres
+sed -i '/^database/,+4d' /data2/homeserver.yaml
+
+# yes, the empty line is needed
+cat <<EOF >> /data2/homeserver.yaml
+
+database:
+  name: psycopg2
+  args:
+    user: synapse_user
+    database: synapse
+    host: localhost
+    cp_min: 5
+    cp_max: 10
+
+rc_message:
+  per_second: 10000
+  burst_count: 100000
+
+rc_registration:
+  per_second: 10000
+  burst_count: 30000
+
+rc_login:
+  address:
+    per_second: 10000
+    burst_count: 30000
+  account:
+    per_second: 10000
+    burst_count: 30000
+  failed_attempts:
+    per_second: 10000
+    burst_count: 30000
+
+rc_admin_redaction:
+  per_second: 1000
+  burst_count: 5000
+
+rc_joins:
+  local:
+    per_second: 10000
+    burst_count: 100000
+  remote:
+    per_second: 10000
+    burst_count: 100000
+EOF
+
+# start synapse and create users
+/start.py &
+
+echo Waiting for synapse to start...
+until curl -s -f -k https://localhost:8008/_matrix/client/versions; do echo "Checking ..."; sleep 2; done
+echo Register alice
+register_new_matrix_user --admin -u alice -p secret -c /data2/homeserver.yaml https://localhost:8008
+echo Register bob
+register_new_matrix_user --admin -u bob -p secret -c /data2/homeserver.yaml https://localhost:8008
+echo Register carl
+register_new_matrix_user --admin -u carl -p secret -c /data2/homeserver.yaml https://localhost:8008
+
+exit 0
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d06a826b95f4dd09c71fc17a907a566153d19fd6..705d7764579d50537d0c23ad2482c1699c2c6fed 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,126 @@
+variables:
+  CCACHE_COMPILERCHECK: content
+  CCACHE_DIR: "${CI_PROJECT_DIR}/.ccache"
+  # prevent configure tzdata hanging apt install commands
+  DEBIAN_FRONTEND: noninteractive
+
+include:
+  - template: 'Workflows/Branch-Pipelines.gitlab-ci.yml'
+
+stages:
+  - prepare
+  - build
+
+build:
+  stage: prepare
+  tags: [docker]
+  image:
+    name: gcr.io/kaniko-project/executor:debug
+    entrypoint: [""]
+  rules:
+    - if: $CI_COMMIT_BRANCH
+      changes:
+        - .ci/synapse/Dockerfile
+        - .ci/synapse/setup-synapse.sh
+        - .ci/synapse/service/synapse/*
+        - .ci/synapse/service/postgresql/*
+  script:
+    - mkdir -p /kaniko/.docker
+    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
+    - /kaniko/executor --whitelist-var-run=false --context $CI_PROJECT_DIR/.ci/synapse --dockerfile $CI_PROJECT_DIR/.ci/synapse/Dockerfile --destination $CI_REGISTRY_IMAGE/synapse:latest
+
+build-gcc7:
+  stage: build
+  image: ubuntu:16.04
+  tags: [docker]
+  services:
+    - name: $CI_REGISTRY_IMAGE/synapse:latest
+      alias: synapse
+  variables:
+    CXX: g++-8
+    CC: gcc-8
+    TRAVIS_OS_NAME: linux
+  before_script:
+    - apt-get update
+    - apt-get install -y software-properties-common
+    - add-apt-repository ppa:ubuntu-toolchain-r/test -y
+    - apt-get update && apt-get -y install --no-install-recommends ${CXX} ${CC} build-essential ninja-build libssl-dev git ccache curl
+    # need recommended deps for wget
+    - apt-get -y install wget
+    - wget https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0-Linux-x86_64.sh && sh cmake-3.19.0-Linux-x86_64.sh  --skip-license  --prefix=/usr/local
+    - /usr/sbin/update-ccache-symlinks
+    - update-alternatives --install /usr/bin/gcc gcc "/usr/bin/${CC}" 10
+    - update-alternatives --install /usr/bin/g++ g++ "/usr/bin/${CXX}" 10
+    - update-alternatives --set gcc "/usr/bin/${CC}"
+    - update-alternatives --set g++ "/usr/bin/${CXX}"
+  script:
+    - curl -s -f -k https://synapse:8008/_matrix/client/versions
+    - export PATH="/usr/lib/ccache:${PATH}"
+    - export CMAKE_BUILD_PARALLEL_LEVEL=$(cat /proc/cpuinfo | awk '/^processor/{print $3}' | wc -l)
+    - export PATH="/usr/local/bin/:${PATH}"
+    - mkdir -p .deps/usr .hunter
+    - mkdir -p build
+    - cmake -GNinja -H. -Bbuild
+        -DCMAKE_INSTALL_PREFIX=.deps/usr
+        -DHUNTER_ROOT=".hunter"
+        -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
+        -DCMAKE_BUILD_TYPE=Release -DHUNTER_CONFIGURATION_TYPES=Debug
+        -DCI_BUILD=ON
+    - cmake --build build
+    - MTXCLIENT_SERVER=synapse GTEST_OUTPUT=xml:junit-output/ make test
+  cache:
+    key: "$CI_JOB_NAME"
+    paths:
+      - .hunter/
+      - .ccache
+  artifacts:
+    reports:
+      junit: build/junit-output/*.xml
+    paths: 
+      - build/junit-output/*.xml
+
+build-macos:
+  stage: build
+  tags: [macos]
+  needs: []
+  before_script:
+    - brew update
+    - brew bundle --file=./.ci/macos/Brewfile
+  script:
+    - export PATH=/usr/local/opt/qt/bin/:${PATH}
+    - cmake -GNinja -H. -Bbuild
+        -DCMAKE_BUILD_TYPE=RelWithDebInfo
+        -DCMAKE_INSTALL_PREFIX=.deps/usr
+        -DHUNTER_ROOT=".hunter"
+        -DHUNTER_ENABLED=ON -DBUILD_SHARED_LIBS=OFF
+        -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHUNTER_CONFIGURATION_TYPES=RelWithDebInfo
+        -DUSE_BUNDLED_OPENSSL=ON
+        -DUSE_BUNDLED_BOOST=ON
+        -DCI_BUILD=ON
+    - cmake --build build
+  cache:
+    key: "${CI_JOB_NAME}"
+    paths:
+      - .hunter/
+      - "${CCACHE_DIR}"
+
+linting:
+  stage: build
+  image: alpine:latest
+  tags: [docker]
+  needs: []
+  before_script:
+    - apk update && apk add clang make git
+  script:
+    - make lint
+
 test-pages:
+  stage: build
   tags: [docker]
   image: alpine
   except:
     - master
+  needs: []
   before_script:
     - apk update
     - apk add doxygen git texlive-full py3-jinja2 py3-pygments
@@ -15,10 +133,12 @@ test-pages:
       - public
   
 pages:
+  stage: build
   tags: [docker]
   image: alpine
   only:
     - master
+  needs: []
   before_script:
     - apk update
     - apk add doxygen git texlive-full py3-jinja2 py3-pygments
@@ -29,3 +149,4 @@ pages:
   artifacts:
     paths:
       - public
+
diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp
index 29e3d6f8c59830430f67f647e47011b65ae19bd3..d192c061f3a62ecb5e4f1dbb218b5a1199960968 100644
--- a/include/mtx/requests.hpp
+++ b/include/mtx/requests.hpp
@@ -255,7 +255,8 @@ struct KeySignaturesUpload
 void
 to_json(json &obj, const KeySignaturesUpload &req);
 
-struct PusherData {
+struct PusherData
+{
         //! Required if `kind` is http. The URL to use to send notifications to.
         //! MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
         std::string url;
@@ -270,7 +271,8 @@ void
 to_json(json &obj, const PusherData &data);
 
 //! Request payload for the `POST /_matrix/client/r0/pushers/set` endpoint.
-struct SetPusher {
+struct SetPusher
+{
         //! Required. Unique identifier for this pusher.
         std::string pushkey;
         //! Required. The kind of pusher to configure. "http" makes a pusher that sends HTTP pokes.
@@ -280,7 +282,8 @@ struct SetPusher {
         //! Required. This is a reverse-DNS style identifier for the application.
         //! If the `kind` is "email", this is "m.email".
         std::string app_id;
-        //! Required. A string that will allow the user to identify what application owns this pusher.
+        //! Required. A string that will allow the user to identify what application owns this
+        //! pusher.
         std::string app_display_name;
         //! Required. A string that will allow the user to identify what device owns this pusher.
         std::string device_display_name;
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 67987f807801a137939c326b5b8e18c93a41e3bd..14824fb0645408bd7fa7ccd0e47a46adaaf68232 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -561,8 +561,7 @@ public:
         void get_turn_server(Callback<mtx::responses::TurnServer> cb);
 
         //! Sets, updates, or deletes a pusher
-        void set_pusher(const mtx::requests::SetPusher &req,
-                        Callback<mtx::responses::Empty> cb);
+        void set_pusher(const mtx::requests::SetPusher &req, Callback<mtx::responses::Empty> cb);
 
 private:
         template<class Request, class Response>
diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp
index f23d67db5cef97b4b8c108f16957f46c6be82a61..546272fa244c06f6730d85b6548014354e70cbcb 100644
--- a/lib/structs/requests.cpp
+++ b/lib/structs/requests.cpp
@@ -187,7 +187,6 @@ to_json(json &obj, const KeySignaturesUpload &req)
                           std::visit([](const auto &e) { return json(e); }, keyVar);
 }
 
-
 void
 to_json(json &obj, const PusherData &data)
 {
@@ -202,16 +201,16 @@ to_json(json &obj, const PusherData &data)
 void
 to_json(json &obj, const SetPusher &req)
 {
-        obj["pushkey"] = req.pushkey;
-        obj["kind"] = req.kind;
-        obj["app_id"] = req.app_id;
-        obj["app_display_name"] = req.app_display_name;
+        obj["pushkey"]             = req.pushkey;
+        obj["kind"]                = req.kind;
+        obj["app_id"]              = req.app_id;
+        obj["app_display_name"]    = req.app_display_name;
         obj["device_display_name"] = req.device_display_name;
         if (!req.profile_tag.empty()) {
                 obj["profile_tag"] = req.profile_tag;
         }
-        obj["lang"] = req.lang;
-        obj["data"] = req.data;
+        obj["lang"]   = req.lang;
+        obj["data"]   = req.data;
         obj["append"] = req.append;
 }
 
diff --git a/tests/client_api.cpp b/tests/client_api.cpp
index 9332f5d8282574b13ccf2e9050a81f56c9fcbf9e..b6f153efd859848b777517e2c453c93b64c4a0fa 100644
--- a/tests/client_api.cpp
+++ b/tests/client_api.cpp
@@ -50,7 +50,7 @@ TEST(ClientAPI, Register)
                     "secret",
                     {err->matrix_error.unauthorized.session, mtx::user_interactive::auth::Dummy{}},
                     [username](const mtx::responses::Register &res, RequestErr err) {
-                            const auto user_id = "@" + username + ":localhost";
+                            const auto user_id = "@" + username + ":" + server_name();
 
                             check_error(err);
                             EXPECT_EQ(res.user_id.to_string(), user_id);
@@ -66,17 +66,17 @@ TEST(ClientAPI, LoginSuccess)
 
         mtx_client->login("alice", "secret", [](const mtx::responses::Login &res, RequestErr err) {
                 check_error(err);
-                validate_login("@alice:localhost", res);
+                validate_login("@alice:" + server_name(), res);
         });
 
         mtx_client->login("bob", "secret", [](const mtx::responses::Login &res, RequestErr err) {
                 check_error(err);
-                validate_login("@bob:localhost", res);
+                validate_login("@bob:" + server_name(), res);
         });
 
         mtx_client->login("carl", "secret", [](const mtx::responses::Login &res, RequestErr err) {
                 check_error(err);
-                validate_login("@carl:localhost", res);
+                validate_login("@carl:" + server_name(), res);
         });
 
         mtx_client->close();
@@ -268,7 +268,7 @@ TEST(ClientAPI, CreateRoom)
         mtx_client->create_room(req, [](const mtx::responses::CreateRoom &res, RequestErr err) {
                 check_error(err);
                 ASSERT_TRUE(res.room_id.localpart().size() > 10);
-                EXPECT_EQ(res.room_id.hostname(), "localhost");
+                EXPECT_EQ(res.room_id.hostname(), server_name());
         });
 
         mtx_client->close();
@@ -441,7 +441,7 @@ TEST(ClientAPI, CreateRoomInvites)
         mtx::requests::CreateRoom req;
         req.name   = "Name";
         req.topic  = "Topic";
-        req.invite = {"@bob:localhost", "@carl:localhost"};
+        req.invite = {"@bob:" + server_name(), "@carl:" + server_name()};
         alice->create_room(req, [bob, carl](const mtx::responses::CreateRoom &res, RequestErr err) {
                 check_error(err);
                 auto room_id = res.room_id.to_string();
@@ -483,7 +483,7 @@ TEST(ClientAPI, JoinRoom)
         mtx::requests::CreateRoom req;
         req.name            = "Name";
         req.topic           = "Topic";
-        req.invite          = {"@bob:localhost"};
+        req.invite          = {"@bob:" + server_name()};
         req.room_alias_name = alias;
         alice->create_room(
           req, [bob, alias](const mtx::responses::CreateRoom &res, RequestErr err) {
@@ -495,7 +495,7 @@ TEST(ClientAPI, JoinRoom)
                   });
 
                   using namespace mtx::identifiers;
-                  bob->join_room("!random_room_id:localhost",
+                  bob->join_room("!random_room_id:" + server_name(),
                                  [](const mtx::responses::RoomId &, RequestErr err) {
                                          ASSERT_TRUE(err);
                                          EXPECT_EQ(
@@ -505,7 +505,7 @@ TEST(ClientAPI, JoinRoom)
 
                   // Join the room using an alias.
                   bob->join_room(
-                    "#" + alias + ":localhost",
+                    "#" + alias + ":" + server_name(),
                     [](const mtx::responses::RoomId &, RequestErr err) { check_error(err); });
           });
 
@@ -532,7 +532,7 @@ TEST(ClientAPI, LeaveRoom)
         mtx::requests::CreateRoom req;
         req.name   = "Name";
         req.topic  = "Topic";
-        req.invite = {"@bob:localhost"};
+        req.invite = {"@bob:" + server_name()};
         alice->create_room(req, [bob](const mtx::responses::CreateRoom &res, RequestErr err) {
                 check_error(err);
                 auto room_id = res.room_id;
@@ -549,11 +549,12 @@ TEST(ClientAPI, LeaveRoom)
         });
 
         // Trying to leave a non-existent room should fail.
-        bob->leave_room("!random_room_id:localhost", [](mtx::responses::Empty, RequestErr err) {
-                ASSERT_TRUE(err);
-                EXPECT_EQ(mtx::errors::to_string(err->matrix_error.errcode), "M_UNKNOWN");
-                EXPECT_EQ(err->matrix_error.error, "Not a known room");
-        });
+        bob->leave_room(
+          "!random_room_id:" + server_name(), [](mtx::responses::Empty, RequestErr err) {
+                  ASSERT_TRUE(err);
+                  EXPECT_EQ(mtx::errors::to_string(err->matrix_error.errcode), "M_UNKNOWN");
+                  EXPECT_EQ(err->matrix_error.error, "Not a known room");
+          });
 
         alice->close();
         bob->close();
@@ -585,7 +586,7 @@ TEST(ClientAPI, InviteRoom)
                   auto room_id = res.room_id.to_string();
 
                   alice->invite_user(room_id,
-                                     "@bob:localhost",
+                                     "@bob:" + server_name(),
                                      [room_id, bob](const mtx::responses::Empty &, RequestErr err) {
                                              check_error(err);
 
@@ -628,7 +629,7 @@ TEST(ClientAPI, KickRoom)
 
                   alice->invite_user(
                     room_id,
-                    "@bob:localhost",
+                    "@bob:" + server_name(),
                     [room_id, alice, bob](const mtx::responses::Empty &, RequestErr err) {
                             check_error(err);
 
@@ -638,7 +639,7 @@ TEST(ClientAPI, KickRoom)
                                       check_error(err);
 
                                       alice->kick_user(room_id,
-                                                       "@bob:localhost",
+                                                       "@bob:" + server_name(),
                                                        [](const mtx::responses::Empty &,
                                                           RequestErr err) { check_error(err); });
                               });
@@ -676,7 +677,7 @@ TEST(ClientAPI, BanRoom)
 
                   alice->invite_user(
                     room_id,
-                    "@bob:localhost",
+                    "@bob:" + server_name(),
                     [room_id, alice, bob](const mtx::responses::Empty &, RequestErr err) {
                             check_error(err);
 
@@ -687,13 +688,13 @@ TEST(ClientAPI, BanRoom)
 
                                       alice->ban_user(
                                         room_id,
-                                        "@bob:localhost",
+                                        "@bob:" + server_name(),
                                         [alice, room_id](const mtx::responses::Empty &,
                                                          RequestErr err) {
                                                 check_error(err);
                                                 alice->unban_user(
                                                   room_id,
-                                                  "@bob:localhost",
+                                                  "@bob:" + server_name(),
                                                   [](const mtx::responses::Empty &,
                                                      RequestErr err) { check_error(err); },
                                                   "You not bad anymore!");
@@ -733,7 +734,7 @@ TEST(ClientAPI, InvalidInvite)
                   auto room_id = res.room_id.to_string();
 
                   bob->invite_user(room_id,
-                                   "@carl:localhost",
+                                   "@carl:" + server_name(),
                                    [room_id, bob](const mtx::responses::Empty &, RequestErr err) {
                                            ASSERT_TRUE(err);
                                            EXPECT_EQ(
@@ -836,7 +837,7 @@ TEST(ClientAPI, Typing)
                                       mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
                                       room.ephemeral.events.front())
                                       .content.user_ids.front(),
-                                    "@alice:localhost");
+                                    "@alice:" + server_name());
                           });
 
                         while (!can_continue)
@@ -926,7 +927,7 @@ TEST(ClientAPI, PresenceOverSync)
                 sleep();
 
         mtx::requests::CreateRoom req;
-        req.invite = {"@bob:localhost"};
+        req.invite = {"@bob:" + server_name()};
         alice->create_room(
           req, [alice, bob](const mtx::responses::CreateRoom &res, RequestErr err) {
                   check_error(err);
@@ -960,7 +961,7 @@ TEST(ClientAPI, PresenceOverSync)
                                                           bool found = false;
                                                           for (const auto &p : s.presence) {
                                                                   if (p.sender ==
-                                                                      "@alice:localhost") {
+                                                                      "@alice:" + server_name()) {
                                                                           found = true;
                                                                           EXPECT_EQ(
                                                                             p.content.presence,
@@ -998,7 +999,7 @@ TEST(ClientAPI, SendMessages)
                 sleep();
 
         mtx::requests::CreateRoom req;
-        req.invite = {"@bob:localhost"};
+        req.invite = {"@bob:" + server_name()};
         alice->create_room(
           req, [alice, bob](const mtx::responses::CreateRoom &res, RequestErr err) {
                   check_error(err);
@@ -1114,7 +1115,7 @@ TEST(ClientAPI, SendStateEvents)
                 sleep();
 
         mtx::requests::CreateRoom req;
-        req.invite = {"@bob:localhost"};
+        req.invite = {"@bob:" + server_name()};
         alice->create_room(
           req, [alice, bob](const mtx::responses::CreateRoom &res, RequestErr err) {
                   check_error(err);
@@ -1299,7 +1300,7 @@ TEST(ClientAPI, ReadMarkers)
                               receipts.front())
                               .content.receipts[event_id];
                           EXPECT_EQ(users.users.size(), 1);
-                          ASSERT_TRUE(users.users["@alice:localhost"].ts > 0);
+                          ASSERT_TRUE(users.users["@alice:" + server_name()].ts > 0);
                   });
         });
 
@@ -1351,7 +1352,7 @@ TEST(ClientAPI, SendToDevice)
                         EXPECT_EQ(event.content.request_id, "test_request_id");
                         EXPECT_EQ(event.content.requesting_device_id, "test_req_id");
                         EXPECT_EQ(event.type, mtx::events::EventType::RoomKeyRequest);
-                        EXPECT_EQ(event.sender, "@alice:localhost");
+                        EXPECT_EQ(event.sender, "@alice:" + server_name());
                 });
         });
 
@@ -1452,13 +1453,13 @@ TEST(ClientAPI, RetrieveSingleEvent)
                                     auto e =
                                       std::get<mtx::events::RoomEvent<mtx::events::msg::Text>>(res);
                                     EXPECT_EQ(e.content.body, "Hello Alice!");
-                                    EXPECT_EQ(e.sender, "@bob:localhost");
+                                    EXPECT_EQ(e.sender, "@bob:" + server_name());
                                     EXPECT_EQ(e.event_id, event_id);
                             });
 
                           bob->get_event(
                             room_id,
-                            "$random_event:localhost",
+                            "$random_event:" + server_name(),
                             [event_id = res.event_id.to_string()](
                               const mtx::events::collections::TimelineEvents &, RequestErr err) {
                                     ASSERT_TRUE(err);
@@ -1516,21 +1517,21 @@ TEST(Groups, Rooms)
 
                   WAIT_UNTIL(rooms_added == 2)
 
-                  alice->joined_groups(
-                    [random_group_id](const mtx::responses::JoinedGroups &res, RequestErr err) {
-                            check_error(err);
+                  alice->joined_groups([random_group_id](const mtx::responses::JoinedGroups &res,
+                                                         RequestErr err) {
+                          check_error(err);
 
-                            ASSERT_GE(res.groups.size(), 1);
+                          ASSERT_GE(res.groups.size(), 1);
 
-                            for (const auto &g : res.groups) {
-                                    if (g == std::string("+" + random_group_id + ":localhost"))
-                                            return;
-                            }
+                          for (const auto &g : res.groups) {
+                                  if (g == std::string("+" + random_group_id + ":" + server_name()))
+                                          return;
+                          }
 
-                            FAIL();
-                    });
+                          FAIL();
+                  });
 
-                  alice->group_rooms("+" + random_group_id + ":localhost",
+                  alice->group_rooms("+" + random_group_id + ":" + server_name(),
                                      [](const nlohmann::json &res, RequestErr err) {
                                              check_error(err);
                                              EXPECT_GE(res.at("chunk").size(), 2);
@@ -1558,13 +1559,13 @@ TEST(Groups, Profiles)
                   json profile;
                   profile["name"] = "Name";
                   alice->set_group_profile(
-                    "+" + random_group_id + ":localhost",
+                    "+" + random_group_id + ":" + server_name(),
                     profile,
                     [alice, random_group_id](const nlohmann::json &, RequestErr err) {
                             check_error(err);
 
                             alice->group_profile(
-                              "+" + random_group_id + ":localhost",
+                              "+" + random_group_id + ":" + server_name(),
                               [](const mtx::responses::GroupProfile &res, RequestErr err) {
                                       check_error(err);
                                       EXPECT_EQ(res.name, "Name");
@@ -1597,7 +1598,7 @@ TEST(ClientAPI, PublicRooms)
         req.name            = "Public Room";
         req.topic           = "Test";
         req.visibility      = mtx::common::RoomVisibility::Public;
-        req.invite          = {"@bob:localhost"};
+        req.invite          = {"@bob:" + server_name()};
         req.room_alias_name = alice->generate_txn_id();
         req.preset          = Preset::PublicChat;
 
@@ -1695,13 +1696,13 @@ TEST(ClientAPI, PublicRooms)
                                                                               check_error(err);
                                                                       });
                                                             },
-                                                            "localhost",
+                                                            server_name(),
                                                             1);
                                                   },
-                                                  "localhost",
+                                                  server_name(),
                                                   1);
                                         },
-                                        "localhost");
+                                        server_name());
                               });
                     });
           });
diff --git a/tests/connection.cpp b/tests/connection.cpp
index 6a78e0e83c72c12a3506d9a084534d2784cc7c89..02edb88b0d6fadb147e411fce39dceeb8dee5006 100644
--- a/tests/connection.cpp
+++ b/tests/connection.cpp
@@ -23,12 +23,13 @@ TEST(Basic, Connection)
 
 TEST(Basic, ServerWithPort)
 {
-        auto alice = std::make_shared<Client>("matrix.org");
+        std::string server = server_name();
+        auto alice         = std::make_shared<Client>("matrix.org");
         alice->verify_certificates(false);
-        alice->set_server("localhost:8448");
+        alice->set_server(server + ":8008");
 
-        EXPECT_EQ(alice->server(), "localhost");
-        EXPECT_EQ(alice->port(), 8448);
+        EXPECT_EQ(alice->server(), server);
+        EXPECT_EQ(alice->port(), 8008);
 
         alice->versions(
           [](const mtx::responses::Versions &, RequestErr err) { ASSERT_FALSE(err); });
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index d828ea9c7c2c808ddaae82b08aa9ce151e16412b..20b0e339999ae2c3cfc8943a513f207b5ef1e3dc 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -551,7 +551,7 @@ TEST(Encryption, EnableEncryption)
         mtx::identifiers::Room joined_room;
 
         mtx::requests::CreateRoom req;
-        req.invite = {"@carl:localhost"};
+        req.invite = {"@carl:" + server_name()};
         bob->create_room(
           req,
           [bob, carl, &responses, &joined_room](const mtx::responses::CreateRoom &res,
@@ -852,7 +852,7 @@ TEST(Encryption, OlmRoomKeyEncryption)
         auto out_session = alice_olm->create_outbound_session(bob_curve25519, bob_otk);
         auto device_msg  = alice_olm->create_olm_encrypted_content(out_session.get(),
                                                                   payload,
-                                                                  UserId("@bob:localhost"),
+                                                                  UserId("@bob:" + server_name()),
                                                                   bob_olm->identity_keys().ed25519,
                                                                   bob_curve25519);
 
@@ -1039,7 +1039,7 @@ TEST(Encryption, ShareSecret)
                           auto device_msg = alice_olm->create_olm_encrypted_content(
                             out_session.get(),
                             payload,
-                            UserId("@bob:localhost"),
+                            UserId("@bob:" + server_name()),
                             bob_olm->identity_keys().ed25519,
                             bob_curve25519);
 
diff --git a/tests/pushrules.cpp b/tests/pushrules.cpp
index 1c8f9c0a7187889eaad1de2681aea3b545db7449..1017b9e00a404d9a15ac82a8d03854310a81b18f 100644
--- a/tests/pushrules.cpp
+++ b/tests/pushrules.cpp
@@ -236,7 +236,7 @@ TEST(Pushrules, GetGlobalRuleset)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   client->get_pushrules([](const mtx::pushrules::GlobalRuleset &, RequestErr err) {
                           EXPECT_TRUE(!err);
@@ -252,7 +252,7 @@ TEST(Pushrules, GetRuleset)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   client->get_pushrules("global",
                                         "content",
@@ -272,7 +272,7 @@ TEST(Pushrules, PutAndDeleteRuleset)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   mtx::pushrules::PushRule rule;
                   rule.pattern = "cake";
@@ -302,7 +302,7 @@ TEST(Pushrules, RulesetEnabled)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   client->get_pushrules_enabled(
                     "global",
@@ -349,7 +349,7 @@ TEST(Pushrules, Actions)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   mtx::pushrules::actions::Actions actions = {
                     {mtx::pushrules::actions::notify{},
@@ -383,7 +383,7 @@ TEST(Pushrules, RoomRuleMute)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   mtx::requests::CreateRoom req;
                   req.name  = "Name";
@@ -393,7 +393,7 @@ TEST(Pushrules, RoomRuleMute)
                     req, [client](const mtx::responses::CreateRoom &res, RequestErr err) {
                             check_error(err);
                             ASSERT_TRUE(res.room_id.localpart().size() > 10);
-                            EXPECT_EQ(res.room_id.hostname(), "localhost");
+                            EXPECT_EQ(res.room_id.hostname(), server_name());
 
                             mtx::pushrules::PushRule rule;
                             rule.actions = {mtx::pushrules::actions::dont_notify{}};
@@ -423,7 +423,7 @@ TEST(Pushrules, RoomRuleMentions)
         client->login(
           "alice", "secret", [client](const mtx::responses::Login &res, RequestErr err) {
                   check_error(err);
-                  validate_login("@alice:localhost", res);
+                  validate_login("@alice:" + server_name(), res);
 
                   mtx::requests::CreateRoom req;
                   req.name  = "Name";
@@ -433,7 +433,7 @@ TEST(Pushrules, RoomRuleMentions)
                     req, [client](const mtx::responses::CreateRoom &res, RequestErr err) {
                             check_error(err);
                             ASSERT_TRUE(res.room_id.localpart().size() > 10);
-                            EXPECT_EQ(res.room_id.hostname(), "localhost");
+                            EXPECT_EQ(res.room_id.hostname(), server_name());
 
                             mtx::pushrules::PushRule rule;
                             rule.actions = {mtx::pushrules::actions::dont_notify{}};
diff --git a/tests/test_helpers.hpp b/tests/test_helpers.hpp
index 7ff864aeb8f266a7ee29dc9e79eccb5a74971ae9..398c52d3af7fce3df14176afca897ddc3021b909 100644
--- a/tests/test_helpers.hpp
+++ b/tests/test_helpers.hpp
@@ -50,11 +50,17 @@ check_error(mtx::http::RequestErr err)
         ASSERT_FALSE(err);
 }
 
+inline std::string
+server_name()
+{
+        const char *server_ = std::getenv("MTXCLIENT_SERVER");
+        return server_ ? server_ : std::string("localhost");
+}
+
 inline auto
 make_test_client()
 {
-        const char *server = std::getenv("MTXCLIENT_SERVER");
-        auto client        = std::make_shared<mtx::http::Client>(server ? server : "localhost");
+        auto client = std::make_shared<mtx::http::Client>(server_name(), 8008);
         client->verify_certificates(false);
         return client;
 }