diff --git a/src/api/client_server/account.rs b/src/api/client_server/account.rs
index 9c00c36f1b396a79877df6d0b2238494c41bda27..f74cdaa7de1773e474cbdc88b0d87f1c48ea8330 100644
--- a/src/api/client_server/account.rs
+++ b/src/api/client_server/account.rs
@@ -83,7 +83,7 @@ pub async fn get_register_available_route(
 ///   access_token
 #[allow(clippy::doc_markdown)]
 pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
-	if !services().globals.allow_registration() && !body.from_appservice {
+	if !services().globals.allow_registration() && body.appservice_info.is_none() {
 		info!(
 			"Registration disabled and request not from known appservice, rejecting registration attempt for username \
 			 {:?}",
@@ -92,10 +92,6 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 		return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
 	}
 
-	if body.body.login_type == Some(LoginType::ApplicationService) && !body.from_appservice {
-		return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing Appservice token."));
-	}
-
 	let is_guest = body.kind == RegistrationKind::Guest;
 
 	if is_guest
@@ -160,6 +156,18 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 		},
 	};
 
+	if body.body.login_type == Some(LoginType::ApplicationService) {
+		if let Some(ref info) = body.appservice_info {
+			if !info.is_user_match(&user_id) {
+				return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
+			}
+		} else {
+			return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
+		}
+	} else if services().appservice.is_exclusive_user_id(&user_id).await {
+		return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
+	}
+
 	// UIAA
 	let mut uiaainfo;
 	let skip_auth;
@@ -174,7 +182,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 			session: None,
 			auth_error: None,
 		};
-		skip_auth = body.from_appservice;
+		skip_auth = body.appservice_info.is_some();
 	} else {
 		// No registration token necessary, but clients must still go through the flow
 		uiaainfo = UiaaInfo {
@@ -186,7 +194,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 			session: None,
 			auth_error: None,
 		};
-		skip_auth = body.from_appservice || is_guest;
+		skip_auth = body.appservice_info.is_some() || is_guest;
 	}
 
 	if !skip_auth {
@@ -281,7 +289,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 	info!("New user \"{}\" registered on this server.", user_id);
 
 	// log in conduit admin channel if a non-guest user registered
-	if !body.from_appservice && !is_guest {
+	if body.appservice_info.is_none() && !is_guest {
 		services()
 			.admin
 			.send_message(RoomMessageEventContent::notice_plain(format!(
@@ -290,7 +298,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 	}
 
 	// log in conduit admin channel if a guest registered
-	if !body.from_appservice && is_guest && services().globals.log_guest_registrations() {
+	if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() {
 		if let Some(device_display_name) = &body.initial_device_display_name {
 			if body
 				.initial_device_display_name
@@ -339,7 +347,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
 		}
 	}
 
-	if !body.from_appservice
+	if body.appservice_info.is_none()
 		&& !services().globals.config.auto_join_rooms.is_empty()
 		&& (services().globals.allow_guests_auto_join_rooms() || !is_guest)
 	{
@@ -468,7 +476,7 @@ pub async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3:
 	Ok(whoami::v3::Response {
 		user_id: sender_user.clone(),
 		device_id,
-		is_guest: services().users.is_deactivated(sender_user)? && !body.from_appservice,
+		is_guest: services().users.is_deactivated(sender_user)? && body.appservice_info.is_none(),
 	})
 }
 
diff --git a/src/api/client_server/alias.rs b/src/api/client_server/alias.rs
index fb57896d44395dd79445f7b7a2c209cb566b6c53..3fafee727247b2dffd47f3ad72f051cd5e274052 100644
--- a/src/api/client_server/alias.rs
+++ b/src/api/client_server/alias.rs
@@ -29,6 +29,18 @@ pub async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> Result
 		return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias is forbidden."));
 	}
 
+	if let Some(ref info) = body.appservice_info {
+		if !info.aliases.is_match(body.room_alias.as_str()) {
+			return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
+		}
+	} else if services()
+		.appservice
+		.is_exclusive_alias(&body.room_alias)
+		.await
+	{
+		return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
+	}
+
 	if services()
 		.rooms
 		.alias
@@ -73,6 +85,18 @@ pub async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result
 		return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist."));
 	}
 
+	if let Some(ref info) = body.appservice_info {
+		if !info.aliases.is_match(body.room_alias.as_str()) {
+			return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
+		}
+	} else if services()
+		.appservice
+		.is_exclusive_alias(&body.room_alias)
+		.await
+	{
+		return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
+	}
+
 	if services()
 		.rooms
 		.alias
diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs
index d2a62576feaf71c738476cdb9e27d72646d0d321..78ba5142b7a35026bf12494c7c86ec4a0fd90120 100644
--- a/src/api/client_server/room.rs
+++ b/src/api/client_server/room.rs
@@ -50,7 +50,10 @@ pub async fn create_room_route(body: Ruma<create_room::v3::Request>) -> Result<c
 
 	let sender_user = body.sender_user.as_ref().expect("user is authenticated");
 
-	if !services().globals.allow_room_creation() && !&body.from_appservice && !services().users.is_admin(sender_user)? {
+	if !services().globals.allow_room_creation()
+		&& body.appservice_info.is_none()
+		&& !services().users.is_admin(sender_user)?
+	{
 		return Err(Error::BadRequest(ErrorKind::forbidden(), "Room creation has been disabled."));
 	}
 
@@ -184,6 +187,16 @@ pub async fn create_room_route(body: Ruma<create_room::v3::Request>) -> Result<c
 			}
 		})?;
 
+	if let Some(ref alias) = alias {
+		if let Some(ref info) = body.appservice_info {
+			if !info.aliases.is_match(alias.as_str()) {
+				return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
+			}
+		} else if services().appservice.is_exclusive_alias(alias).await {
+			return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias reserved by appservice."));
+		}
+	}
+
 	let room_version = match body.room_version.clone() {
 		Some(room_version) => {
 			if services()
diff --git a/src/api/client_server/session.rs b/src/api/client_server/session.rs
index 70ed113c264dcb57d28262aa2ae1107ab01acbf0..215436376b92f77ea78487a0b3b7778b702d8617 100644
--- a/src/api/client_server/session.rs
+++ b/src/api/client_server/session.rs
@@ -66,27 +66,23 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
 			..
 		}) => {
 			debug!("Got password login type");
-			let username = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
-				debug!("Using username from identifier field");
-				user_id.to_lowercase()
-			} else if let Some(user_id) = user {
-				warn!(
-					"User \"{}\" is attempting to login with the deprecated \"user\" field at \
-					 \"/_matrix/client/v3/login\". conduwuit implements this deprecated behaviour, but this is \
-					 destined to be removed in a future Matrix release.",
-					user_id
-				);
-				user_id.to_lowercase()
+			let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
+				UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
+			} else if let Some(user) = user {
+				UserId::parse(user)
 			} else {
 				warn!("Bad login type: {:?}", &body.login_info);
 				return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
-			};
-
-			let user_id = UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
-				warn!("Failed to parse username from user logging in: {}", e);
+			}
+			.map_err(|e| {
+				warn!("Failed to parse username from user logging in: {e}");
 				Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
 			})?;
 
+			if services().appservice.is_exclusive_user_id(&user_id).await {
+				return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
+			}
+
 			let hash = services()
 				.users
 				.password_hash(&user_id)?
@@ -121,16 +117,23 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
 				let token =
 					jsonwebtoken::decode::<Claims>(token, jwt_decoding_key, &jsonwebtoken::Validation::default())
 						.map_err(|e| {
-							warn!("Failed to parse JWT token from user logging in: {}", e);
+							warn!("Failed to parse JWT token from user logging in: {e}");
 							Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid.")
 						})?;
 
 				let username = token.claims.sub.to_lowercase();
 
-				UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
-					warn!("Failed to parse username from user logging in: {}", e);
-					Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
-				})?
+				let user_id =
+					UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
+						warn!("Failed to parse username from user logging in: {e}");
+						Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
+					})?;
+
+				if services().appservice.is_exclusive_user_id(&user_id).await {
+					return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
+				}
+
+				user_id
 			} else {
 				return Err(Error::BadRequest(
 					ErrorKind::Unknown,
@@ -144,27 +147,28 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
 			user,
 		}) => {
 			debug!("Got appservice login type");
-			if !body.from_appservice {
-				return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing Appservice token."));
-			};
-			let username = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
-				user_id.to_lowercase()
-			} else if let Some(user_id) = user {
-				warn!(
-					"Appservice \"{}\" is attempting to login with the deprecated \"user\" field at \
-					 \"/_matrix/client/v3/login\". conduwuit implements this deprecated behaviour, but this is \
-					 destined to be removed in a future Matrix release.",
-					user_id
-				);
-				user_id.to_lowercase()
+			let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
+				UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
+			} else if let Some(user) = user {
+				UserId::parse(user)
 			} else {
+				warn!("Bad login type: {:?}", &body.login_info);
 				return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
-			};
-
-			UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
-				warn!("Failed to parse username from appservice logging in: {}", e);
+			}
+			.map_err(|e| {
+				warn!("Failed to parse username from appservice logging in: {e}");
 				Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
-			})?
+			})?;
+
+			if let Some(ref info) = body.appservice_info {
+				if !info.is_user_match(&user_id) {
+					return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
+				}
+			} else {
+				return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
+			}
+
+			user_id
 		},
 		_ => {
 			warn!("Unsupported or unknown login type: {:?}", &body.login_info);
diff --git a/src/api/ruma_wrapper/axum.rs b/src/api/ruma_wrapper/axum.rs
index b7bdc03a35360cb4dd51da68dd564143032b58ce..3de3b8420d294cc56777ad402bd1b8b592331e9a 100644
--- a/src/api/ruma_wrapper/axum.rs
+++ b/src/api/ruma_wrapper/axum.rs
@@ -124,7 +124,7 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti
 
 		let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
 
-		let (sender_user, sender_device, sender_servername, from_appservice) = match (metadata.authentication, token) {
+		let (sender_user, sender_device, sender_servername, appservice_info) = match (metadata.authentication, token) {
 			(_, Token::Invalid) => {
 				return Err(Error::BadRequest(
 					ErrorKind::UnknownToken {
@@ -146,21 +146,27 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti
 						UserId::parse,
 					)
 					.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
+
+				if !info.is_user_match(&user_id) {
+					return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
+				}
+
 				if !services().users.exists(&user_id)? {
 					return Err(Error::BadRequest(ErrorKind::forbidden(), "User does not exist."));
 				}
 
-				// TODO: Check if appservice is allowed to be that user
-				(Some(user_id), None, None, true)
+				(Some(user_id), None, None, Some(*info))
+			},
+			(AuthScheme::None | AuthScheme::AppserviceToken, Token::Appservice(info)) => {
+				(None, None, None, Some(*info))
 			},
-			(AuthScheme::None | AuthScheme::AppserviceToken, Token::Appservice(_)) => (None, None, None, true),
 			(AuthScheme::AccessToken, Token::None) => {
 				return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing access token."));
 			},
 			(
 				AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
 				Token::User((user_id, device_id)),
-			) => (Some(user_id), Some(device_id), None, false),
+			) => (Some(user_id), Some(device_id), None, None),
 			(AuthScheme::ServerSignatures, Token::None) => {
 				if !services().globals.allow_federation() {
 					return Err(Error::bad_config("Federation is disabled."));
@@ -234,7 +240,7 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti
 				let pub_key_map = BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]);
 
 				match ruma::signatures::verify_json(&pub_key_map, &request_map) {
-					Ok(()) => (None, None, Some(x_matrix.origin), false),
+					Ok(()) => (None, None, Some(x_matrix.origin), None),
 					Err(e) => {
 						warn!("Failed to verify json request from {}: {e}\n{request_map:?}", x_matrix.origin);
 
@@ -253,7 +259,7 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti
 				}
 			},
 			(AuthScheme::None | AuthScheme::AppserviceToken | AuthScheme::AccessTokenOptional, Token::None) => {
-				(None, None, None, false)
+				(None, None, None, None)
 			},
 			(AuthScheme::ServerSignatures, Token::Appservice(_) | Token::User(_)) => {
 				return Err(Error::BadRequest(
@@ -322,7 +328,7 @@ async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejecti
 			sender_device,
 			sender_servername,
 			json_body,
-			from_appservice,
+			appservice_info,
 		})
 	}
 }
diff --git a/src/api/ruma_wrapper/mod.rs b/src/api/ruma_wrapper/mod.rs
index 2b0cd06ba0ff1ef6e3a694f727acac3fa9aa6b03..0dd3a617e3c2d842d8f393b5d4c8caca751feb4b 100644
--- a/src/api/ruma_wrapper/mod.rs
+++ b/src/api/ruma_wrapper/mod.rs
@@ -2,7 +2,7 @@
 
 use ruma::{api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId};
 
-use crate::Error;
+use crate::{service::appservice::RegistrationInfo, Error};
 
 mod axum;
 
@@ -14,7 +14,7 @@ pub struct Ruma<T> {
 	pub sender_servername: Option<OwnedServerName>,
 	// This is None when body is not a valid string
 	pub json_body: Option<CanonicalJsonValue>,
-	pub from_appservice: bool,
+	pub appservice_info: Option<RegistrationInfo>,
 }
 
 impl<T> Deref for Ruma<T> {
diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs
index cf68db218aa69a1c2bce30b4d4273c2b89082d04..ea387881c4e80f02f0640d339d6efc87a9c0e9a9 100644
--- a/src/service/appservice/mod.rs
+++ b/src/service/appservice/mod.rs
@@ -5,7 +5,10 @@
 pub(crate) use data::Data;
 use futures_util::Future;
 use regex::RegexSet;
-use ruma::api::appservice::{Namespace, Registration};
+use ruma::{
+	api::appservice::{Namespace, Registration},
+	RoomAliasId, RoomId, UserId,
+};
 use tokio::sync::RwLock;
 
 use crate::{services, Result};
@@ -43,6 +46,16 @@ pub fn is_exclusive_match(&self, heystack: &str) -> bool {
 	}
 }
 
+impl RegistrationInfo {
+	pub fn is_user_match(&self, user_id: &UserId) -> bool {
+		self.users.is_match(user_id.as_str()) || self.registration.sender_localpart == user_id.localpart()
+	}
+
+	pub fn is_exclusive_user_match(&self, user_id: &UserId) -> bool {
+		self.users.is_exclusive_match(user_id.as_str()) || self.registration.sender_localpart == user_id.localpart()
+	}
+}
+
 impl TryFrom<Vec<Namespace>> for NamespaceRegex {
 	type Error = regex::Error;
 
@@ -122,6 +135,7 @@ pub fn build(db: &'static dyn Data) -> Result<Self> {
 
 	/// Registers an appservice and returns the ID to the caller
 	pub async fn register_appservice(&self, yaml: Registration) -> Result<String> {
+		//TODO: Check for collisions between exclusive appservice namespaces
 		services()
 			.appservice
 			.registration_info
@@ -175,6 +189,30 @@ pub async fn find_from_token(&self, token: &str) -> Option<RegistrationInfo> {
 			.cloned()
 	}
 
+	/// Checks if a given user id matches any exclusive appservice regex
+	pub async fn is_exclusive_user_id(&self, user_id: &UserId) -> bool {
+		self.read()
+			.await
+			.values()
+			.any(|info| info.is_exclusive_user_match(user_id))
+	}
+
+	/// Checks if a given room alias matches any exclusive appservice regex
+	pub async fn is_exclusive_alias(&self, alias: &RoomAliasId) -> bool {
+		self.read()
+			.await
+			.values()
+			.any(|info| info.aliases.is_exclusive_match(alias.as_str()))
+	}
+
+	/// Checks if a given room id matches any exclusive appservice regex
+	pub async fn is_exclusive_room_id(&self, room_id: &RoomId) -> bool {
+		self.read()
+			.await
+			.values()
+			.any(|info| info.rooms.is_exclusive_match(room_id.as_str()))
+	}
+
 	pub fn read(&self) -> impl Future<Output = tokio::sync::RwLockReadGuard<'_, BTreeMap<String, RegistrationInfo>>> {
 		self.registration_info.read()
 	}