diff --git a/Cargo.lock b/Cargo.lock
index b2a47390..c5f2fa2e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -432,6 +432,8 @@ dependencies = [
  "tracing-flame",
  "tracing-opentelemetry",
  "tracing-subscriber",
+ "trust-dns-resolver",
+ "url",
 ]
 
 [[package]]
@@ -3124,6 +3126,7 @@ dependencies = [
  "form_urlencoded",
  "idna 0.5.0",
  "percent-encoding",
+ "serde",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 88383391..eb7463ca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -107,6 +107,9 @@ futures-util = { version = "0.3.28", default-features = false }
 # Used for reading the configuration from conduit.toml & environment variables
 figment = { version = "0.10.8", features = ["env", "toml"] }
 
+# Validating urls in config
+url = { version = "2", features = ["serde"] }
+
 tikv-jemallocator = { version = "0.5.0", features = ["unprefixed_malloc_on_supported_platforms"], optional = true }
 async-trait = "0.1.68"
 
diff --git a/book.toml b/book.toml
index e25746ca..700ecda5 100644
--- a/book.toml
+++ b/book.toml
@@ -16,3 +16,7 @@ git-repository-icon = "fa-git-square"
 
 [output.html.search]
 limit-results = 15
+
+[output.html.code.hidelines]
+json = "~"
+
diff --git a/conduit-example.toml b/conduit-example.toml
index c83bce74..ef7bd182 100644
--- a/conduit-example.toml
+++ b/conduit-example.toml
@@ -17,7 +17,7 @@
 # https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
 # and
 # https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
-# for more information
+# for more information, or continue below to see how conduit can do this for you.
 
 # YOU NEED TO EDIT THIS
 #server_name = "your.server.name"
@@ -65,3 +65,10 @@ trusted_servers = ["matrix.org"]
 
 address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
 #address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
+
+[global.well_known]
+# Conduit handles the /.well-known/matrix/* endpoints, making both clients and servers try to access conduit with the host
+# server_name and port 443 by default.
+# If you want to override these defaults, uncomment and edit the following lines accordingly:
+#server = your.server.name:443
+#client = https://your.server.name
diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md
index f874bb21..afba3cca 100644
--- a/docs/SUMMARY.md
+++ b/docs/SUMMARY.md
@@ -3,6 +3,7 @@
 - [Introduction](introduction.md)
 
 - [Configuration](configuration.md)
+- [Delegation](delegation.md)
 - [Deploying](deploying.md)
     - [Generic](deploying/generic.md)
     - [Debian](deploying/debian.md)
diff --git a/docs/configuration.md b/docs/configuration.md
index efa080dc..d903a21e 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -56,6 +56,7 @@ The `global` section contains the following fields:
 | `turn_secret` | `string` | The TURN secret | `""` |
 | `turn_ttl` | `integer` | The TURN TTL in seconds | `86400` |
 | `emergency_password` | `string` | Set a password to login as the `conduit` user in case of emergency | N/A |
+| `well_known` | `table` | Used for [delegation](delegation.md) | See [delegation](delegation.md) |
 
 
 ### TLS
diff --git a/docs/delegation.md b/docs/delegation.md
new file mode 100644
index 00000000..c8e5391c
--- /dev/null
+++ b/docs/delegation.md
@@ -0,0 +1,69 @@
+# Delegation
+
+You can run Conduit on a separate domain than the actual server name (what shows up in user ids, aliases, etc.).
+For example you can have your users have IDs such as `@foo:example.org` and have aliases like `#bar:example.org`,
+while actually having Conduit hosted on the `matrix.example.org` domain. This is called delegation.
+
+## Automatic (recommended)
+
+Conduit has support for hosting delegation files by itself, and by default uses it to serve federation traffic on port 443.
+
+With this method, you need to direct requests to `/.well-known/matrix/*` to Conduit in your reverse proxy.
+
+This is only recommended if Conduit is on the same physical server as the server which serves your server name (e.g. example.org)
+as servers don't always seem to cache the response, leading to slower response times otherwise, but it should also work if you
+are connected to the server running Conduit using something like a VPN.
+
+> **Note**: this will automatically allow you to use [sliding sync][0] without any extra configuration
+
+To configure it, use the following options in the `global.well_known` table:
+| Field | Type | Description | Default |
+| --- | --- | --- | --- |
+| `client` | `String` | The URL that clients should use to connect to Conduit | `https://<server_name>` |
+| `server` | `String` | The hostname and port servers should use to connect to Conduit | `<server_name>:443` |
+
+### Example
+
+```toml
+[global.well_known]
+client = "https://matrix.example.org"
+server = "matrix.example.org:443"
+```
+
+## Manual
+
+Alternatively you can serve static JSON files to inform clients and servers how to connect to Conduit.
+
+### Servers
+
+For servers to discover how to access your domain, serve a response in the following format for `/.well-known/matrix/server`:
+
+```json
+{
+  "m.server": "matrix.example.org:443"
+}
+```
+Where `matrix.example.org` is the domain and `443` is the port Conduit is accessible at.
+
+### Clients
+
+For clients to discover how to access your domain, serve a response in the following format for `/.well-known/matrix/client`:
+```json
+{
+  "m.homeserver": {
+    "base_url": "https://matrix.example.org"
+  }
+}
+```
+Where `matrix.example.org` is the URL Conduit is accessible at.
+
+To ensure that all clients can access this endpoint, it is recommended you set the following headers for this endpoint:
+```
+Access-Control-Allow-Origin: *
+Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
+Access-Control-Allow-Headers: X-Requested-With, Content-Type, Authorization
+```
+
+If you also want to be able to use [sliding sync][0], look [here](faq.md#how-do-i-setup-sliding-sync).
+
+[0]: https://matrix.org/blog/2023/09/matrix-2-0/#sliding-sync
diff --git a/docs/faq.md b/docs/faq.md
index ce84f818..4c23a25c 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -15,12 +15,16 @@ You can simply stop Conduit, make a copy or file system snapshot of the database
 
 ## How do I setup sliding sync?
 
-You need to add a `org.matrix.msc3575.proxy` field to your `.well-known/matrix/client` response which points to Conduit. Here is an example:
+If you use the [automatic method for delegation](delegation.md#automatic-recommended) or just proxy `.well-known/matrix/client` to Conduit, sliding sync should work with no extra configuration.
+If you don't, continue below.
+
+You need to add a `org.matrix.msc3575.proxy` field to your `.well-known/matrix/client` response which contains a url which Conduit is accessible behind.
+Here is an example:
 ```json
 {
-  "m.homeserver": {
-    "base_url": "https://matrix.example.org"
-  },
+~  "m.homeserver": {
+~    "base_url": "https://matrix.example.org"
+~  },
   "org.matrix.msc3575.proxy": {
     "url": "https://matrix.example.org"
   }
diff --git a/src/api/client_server/mod.rs b/src/api/client_server/mod.rs
index 54c99aa0..afe5181e 100644
--- a/src/api/client_server/mod.rs
+++ b/src/api/client_server/mod.rs
@@ -32,6 +32,7 @@ mod typing;
 mod unversioned;
 mod user_directory;
 mod voip;
+mod well_known;
 
 pub use account::*;
 pub use alias::*;
@@ -67,6 +68,7 @@ pub use typing::*;
 pub use unversioned::*;
 pub use user_directory::*;
 pub use voip::*;
+pub use well_known::*;
 
 pub const DEVICE_ID_LENGTH: usize = 10;
 pub const TOKEN_LENGTH: usize = 32;
diff --git a/src/api/client_server/unversioned.rs b/src/api/client_server/unversioned.rs
index 70e260ec..7706afee 100644
--- a/src/api/client_server/unversioned.rs
+++ b/src/api/client_server/unversioned.rs
@@ -1,9 +1,8 @@
 use std::{collections::BTreeMap, iter::FromIterator};
 
-use axum::{response::IntoResponse, Json};
-use ruma::api::client::{discovery::get_supported_versions, error::ErrorKind};
+use ruma::api::client::discovery::get_supported_versions;
 
-use crate::{services, Error, Result, Ruma};
+use crate::{Result, Ruma};
 
 /// # `GET /_matrix/client/versions`
 ///
@@ -33,18 +32,3 @@ pub async fn get_supported_versions_route(
 
     Ok(resp)
 }
-
-/// # `GET /.well-known/matrix/client`
-pub async fn well_known_client_route(
-    _body: Ruma<get_supported_versions::Request>,
-) -> Result<impl IntoResponse> {
-    let client_url = match services().globals.well_known_client() {
-        Some(url) => url.clone(),
-        None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
-    };
-
-    Ok(Json(serde_json::json!({
-        "m.homeserver": {"base_url": client_url},
-        "org.matrix.msc3575.proxy": {"url": client_url}
-    })))
-}
diff --git a/src/api/client_server/well_known.rs b/src/api/client_server/well_known.rs
new file mode 100644
index 00000000..e7bc2a4a
--- /dev/null
+++ b/src/api/client_server/well_known.rs
@@ -0,0 +1,22 @@
+use ruma::api::client::discovery::discover_homeserver::{
+    self, HomeserverInfo, SlidingSyncProxyInfo,
+};
+
+use crate::{services, Result, Ruma};
+
+/// # `GET /.well-known/matrix/client`
+///
+/// Returns the client server discovery information.
+pub async fn well_known_client(
+    _body: Ruma<discover_homeserver::Request>,
+) -> Result<discover_homeserver::Response> {
+    let client_url = services().globals.well_known_client();
+
+    Ok(discover_homeserver::Response {
+        homeserver: HomeserverInfo {
+            base_url: client_url.clone(),
+        },
+        identity_server: None,
+        sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
+    })
+}
diff --git a/src/api/server_server.rs b/src/api/server_server.rs
index ef6ab4af..d816a3e9 100644
--- a/src/api/server_server.rs
+++ b/src/api/server_server.rs
@@ -17,7 +17,10 @@ use ruma::{
             backfill::get_backfill,
             device::get_devices::{self, v1::UserDevice},
             directory::{get_public_rooms, get_public_rooms_filtered},
-            discovery::{get_server_keys, get_server_version, ServerSigningKeys, VerifyKey},
+            discovery::{
+                discover_homeserver, get_server_keys, get_server_version, ServerSigningKeys,
+                VerifyKey,
+            },
             event::{get_event, get_missing_events, get_room_state, get_room_state_ids},
             keys::{claim_keys, get_keys},
             membership::{create_invite, create_join_event, prepare_join_event},
@@ -1911,6 +1914,17 @@ pub async fn claim_keys_route(
     })
 }
 
+/// # `GET /.well-known/matrix/server`
+///
+/// Returns the federation server discovery information.
+pub async fn well_known_server(
+    _body: Ruma<discover_homeserver::Request>,
+) -> Result<discover_homeserver::Response> {
+    Ok(discover_homeserver::Response {
+        server: services().globals.well_known_server(),
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::{add_port_to_hostname, get_ip_with_port, FedDest};
diff --git a/src/config/mod.rs b/src/config/mod.rs
index fb1e2f31..652b3a4c 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -7,6 +7,7 @@ use std::{
 use ruma::{OwnedServerName, RoomVersionId};
 use serde::{de::IgnoredAny, Deserialize};
 use tracing::warn;
+use url::Url;
 
 mod proxy;
 
@@ -56,7 +57,8 @@ pub struct Config {
     pub allow_unstable_room_versions: bool,
     #[serde(default = "default_default_room_version")]
     pub default_room_version: RoomVersionId,
-    pub well_known_client: Option<String>,
+    #[serde(default)]
+    pub well_known: WellKnownConfig,
     #[serde(default = "false_fn")]
     pub allow_jaeger: bool,
     #[serde(default = "false_fn")]
@@ -91,6 +93,12 @@ pub struct TlsConfig {
     pub key: String,
 }
 
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct WellKnownConfig {
+    pub client: Option<Url>,
+    pub server: Option<OwnedServerName>,
+}
+
 const DEPRECATED_KEYS: &[&str] = &["cache_capacity"];
 
 impl Config {
@@ -111,9 +119,35 @@ impl Config {
     }
 }
 
+impl Config {
+    pub fn well_known_client(&self) -> String {
+        if let Some(url) = &self.well_known.client {
+            url.to_string()
+        } else {
+            format!("https://{}", self.server_name)
+        }
+    }
+
+    pub fn well_known_server(&self) -> OwnedServerName {
+        match &self.well_known.server {
+            Some(server_name) => server_name.to_owned(),
+            None => {
+                if self.server_name.port().is_some() {
+                    self.server_name.to_owned()
+                } else {
+                    format!("{}:443", self.server_name.host())
+                        .try_into()
+                        .expect("Host from valid hostname + :443 must be valid")
+                }
+            }
+        }
+    }
+}
+
 impl fmt::Display for Config {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         // Prepare a list of config values to show
+        let well_known_server = self.well_known_server();
         let lines = [
             ("Server name", self.server_name.host()),
             ("Database backend", &self.database_backend),
@@ -194,6 +228,8 @@ impl fmt::Display for Config {
                 }
                 &lst.join(", ")
             }),
+            ("Well-known server name", well_known_server.as_str()),
+            ("Well-known client URL", &self.well_known_client()),
         ];
 
         let mut msg: String = "Active config values:\n\n".to_owned();
diff --git a/src/main.rs b/src/main.rs
index 7beeb8ba..84467543 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -390,6 +390,7 @@ fn routes(config: &Config) -> Router {
         .ruma_route(client_server::get_relating_events_with_rel_type_route)
         .ruma_route(client_server::get_relating_events_route)
         .ruma_route(client_server::get_hierarchy_route)
+        .ruma_route(client_server::well_known_client)
         .route(
             "/_matrix/client/r0/rooms/:room_id/initialSync",
             get(initial_sync),
@@ -430,10 +431,12 @@ fn routes(config: &Config) -> Router {
             .ruma_route(server_server::get_profile_information_route)
             .ruma_route(server_server::get_keys_route)
             .ruma_route(server_server::claim_keys_route)
+            .ruma_route(server_server::well_known_server)
     } else {
         router
             .route("/_matrix/federation/*path", any(federation_disabled))
             .route("/_matrix/key/*path", any(federation_disabled))
+            .route("/.well-known/matrix/server", any(federation_disabled))
     }
 }
 
diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs
index ab66ed45..263463d7 100644
--- a/src/service/globals/mod.rs
+++ b/src/service/globals/mod.rs
@@ -417,8 +417,12 @@ impl Service {
         r
     }
 
-    pub fn well_known_client(&self) -> &Option<String> {
-        &self.config.well_known_client
+    pub fn well_known_server(&self) -> OwnedServerName {
+        self.config.well_known_server()
+    }
+
+    pub fn well_known_client(&self) -> String {
+        self.config.well_known_client()
     }
 
     pub fn shutdown(&self) {