diff --git a/Cargo.toml b/Cargo.toml
index 5f7a3fde..40a67355 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -258,6 +258,7 @@ features = [
     "unstable-msc2870",
     "unstable-msc3026",
     "unstable-msc3061",
+    "unstable-msc4121",
     "unstable-extensible-events",
 ]
 
diff --git a/conduwuit-example.toml b/conduwuit-example.toml
index cfd0b0e9..045d1c3b 100644
--- a/conduwuit-example.toml
+++ b/conduwuit-example.toml
@@ -51,7 +51,6 @@
 #sentry_traces_sample_rate = 0.15
 
 
-
 ### Database configuration
 
 # This is the only directory where conduwuit will save its data, including media
@@ -63,7 +62,6 @@ database_path = "/var/lib/matrix-conduit/"
 database_backend = "rocksdb"
 
 
-
 ### Network
 
 # The port(s) conduwuit will be running on. You need to set up a reverse proxy such as
@@ -75,7 +73,7 @@ port = 6167
 
 # default address (IPv4 or IPv6) conduwuit will listen on. Generally you want this to be
 # localhost (127.0.0.1 / ::1). If you are using Docker or a container NAT networking setup, you
-# likely need this to be 0.0.0.0. 
+# likely need this to be 0.0.0.0.
 address = "127.0.0.1"
 
 # How many requests conduwuit sends to other servers at the same time concurrently. Default is 500
@@ -153,7 +151,6 @@ ip_range_denylist = [
 ]
 
 
-
 ### Moderation / Privacy / Security
 
 # Set to true to allow user type "guest" registrations. Element attempts to register guest users automatically.
@@ -250,6 +247,13 @@ url_preview_max_spider_size = 384_000
 # Useful if the domain contains allowlist is still too broad for you but you still want to allow all the subdomains under a root domain.
 url_preview_check_root_domain = false
 
+# A single contact and/or support page for /.well-known/matrix/support
+# All options here are strings. Currently only supports 1 single contact.
+# No default.
+#well_known_support_page = ""
+#well_known_support_role = ""
+#well_known_support_email = ""
+#well_known_support_mxid = ""
 
 
 ### Misc
@@ -344,7 +348,6 @@ url_preview_check_root_domain = false
 #cleanup_second_interval = 1800
 
 
-
 ### RocksDB options
 
 # Set this to true to use RocksDB config options that are tailored to HDDs (slower device storage)
@@ -439,7 +442,6 @@ url_preview_check_root_domain = false
 #rocksdb_recovery_mode = 1
 
 
-
 ### Domain Name Resolution and Caching
 
 # Maximum entries stored in DNS memory-cache. The size of an entry may vary so please take care if
@@ -470,7 +472,6 @@ url_preview_check_root_domain = false
 #query_all_nameservers = false
 
 
-
 ### Request Timeouts, Connection Timeouts, and Connection Pooling
 
 ## Request Timeouts are HTTP response timeouts
@@ -548,7 +549,6 @@ url_preview_check_root_domain = false
 #pusher_idle_timeout = 15
 
 
-
 ### Presence / Typing Indicators / Read Receipts
 
 # Config option to control local (your server only) presence updates/requests. Defaults to true.
@@ -597,7 +597,6 @@ url_preview_check_root_domain = false
 #typing_client_timeout_max_s = 45
 
 
-
 # Other options not in [global]:
 #
 #
diff --git a/src/api/client_server/unversioned.rs b/src/api/client_server/unversioned.rs
index 1a530b6e..be2bd75c 100644
--- a/src/api/client_server/unversioned.rs
+++ b/src/api/client_server/unversioned.rs
@@ -1,7 +1,13 @@
 use std::collections::BTreeMap;
 
 use axum::{response::IntoResponse, Json};
-use ruma::api::client::{discovery::get_supported_versions, error::ErrorKind};
+use ruma::api::client::{
+	discovery::{
+		discover_support::{self, Contact},
+		get_supported_versions,
+	},
+	error::ErrorKind,
+};
 
 use crate::{services, Error, Result, Ruma};
 
@@ -62,6 +68,51 @@ pub async fn well_known_client_route() -> Result<impl IntoResponse> {
 	})))
 }
 
+/// # `GET /.well-known/matrix/support`
+///
+/// Server support contact and support page of a homeserver's domain.
+pub async fn well_known_support(_body: Ruma<discover_support::Request>) -> Result<discover_support::Response> {
+	let support_page = services().globals.well_known_support_page().clone();
+
+	let role = services().globals.well_known_support_role().clone();
+
+	// support page or role must be either defined for this to be valid
+	if support_page.is_none() && role.is_none() {
+		return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
+	}
+
+	let email_address = services().globals.well_known_support_email().clone();
+	let matrix_id = services().globals.well_known_support_mxid().clone();
+
+	// if a role is specified, an email address or matrix id is required
+	if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
+		return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
+	}
+
+	// TOOD: support defining multiple contacts in the config
+	let mut contacts: Vec<Contact> = vec![];
+
+	if let Some(role) = role {
+		let contact = Contact {
+			role,
+			email_address,
+			matrix_id,
+		};
+
+		contacts.push(contact);
+	}
+
+	// support page or role+contacts must be either defined for this to be valid
+	if contacts.is_empty() && support_page.is_none() {
+		return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
+	}
+
+	Ok(discover_support::Response {
+		contacts,
+		support_page,
+	})
+}
+
 /// # `GET /client/server.json`
 ///
 /// Endpoint provided by sliding sync proxy used by some clients such as Element
diff --git a/src/config/mod.rs b/src/config/mod.rs
index e0b379df..786168a6 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -15,7 +15,9 @@ use figment::{
 };
 use itertools::Itertools;
 use regex::RegexSet;
-use ruma::{OwnedRoomId, OwnedServerName, RoomVersionId};
+use ruma::{
+	api::client::discovery::discover_support::ContactRole, OwnedRoomId, OwnedServerName, OwnedUserId, RoomVersionId,
+};
 use serde::{de::IgnoredAny, Deserialize};
 use tracing::{debug, error, warn};
 
@@ -254,6 +256,11 @@ pub struct Config {
 	#[serde(default = "default_ip_range_denylist")]
 	pub ip_range_denylist: Vec<String>,
 
+	pub well_known_support_page: Option<String>,
+	pub well_known_support_role: Option<ContactRole>,
+	pub well_known_support_email: Option<String>,
+	pub well_known_support_mxid: Option<OwnedUserId>,
+
 	#[serde(default = "Vec::new")]
 	pub url_preview_domain_contains_allowlist: Vec<String>,
 	#[serde(default = "Vec::new")]
diff --git a/src/routes.rs b/src/routes.rs
index cb43322f..2ae1072f 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -206,6 +206,7 @@ pub fn routes() -> Router {
 		.ruma_route(server_server::get_keys_route)
 		.ruma_route(server_server::claim_keys_route)
         .ruma_route(server_server::get_hierarchy_route)
+        .ruma_route(client_server::well_known_support)
         .route("/_conduwuit/server_version", get(client_server::conduwuit_server_version))
 		.route("/_matrix/client/r0/rooms/:room_id/initialSync", get(initial_sync))
 		.route("/_matrix/client/v3/rooms/:room_id/initialSync", get(initial_sync))
diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs
index b9086e34..d78f5a12 100644
--- a/src/service/globals/mod.rs
+++ b/src/service/globals/mod.rs
@@ -17,7 +17,7 @@ use hickory_resolver::TokioAsyncResolver;
 use regex::RegexSet;
 use ruma::{
 	api::{
-		client::sync::sync_events,
+		client::{discovery::discover_support::ContactRole, sync::sync_events},
 		federation::discovery::{ServerSigningKeys, VerifyKey},
 	},
 	serde::Base64,
@@ -307,6 +307,14 @@ impl Service<'_> {
 
 	pub fn ip_range_denylist(&self) -> &[String] { &self.config.ip_range_denylist }
 
+	pub fn well_known_support_page(&self) -> &Option<String> { &self.config.well_known_support_page }
+
+	pub fn well_known_support_role(&self) -> &Option<ContactRole> { &self.config.well_known_support_role }
+
+	pub fn well_known_support_email(&self) -> &Option<String> { &self.config.well_known_support_email }
+
+	pub fn well_known_support_mxid(&self) -> &Option<OwnedUserId> { &self.config.well_known_support_mxid }
+
 	pub fn block_non_admin_invites(&self) -> bool { self.config.block_non_admin_invites }
 
 	pub fn supported_room_versions(&self) -> Vec<RoomVersionId> {