diff --git a/src/admin/admin.rs b/src/admin/admin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e55f6d61060d85d2f623d46658a6c7a52f03f380
--- /dev/null
+++ b/src/admin/admin.rs
@@ -0,0 +1,66 @@
+use clap::Parser;
+use conduit::Result;
+use ruma::events::room::message::RoomMessageEventContent;
+
+use crate::{
+	appservice, appservice::AppserviceCommand, check, check::CheckCommand, debug, debug::DebugCommand, federation,
+	federation::FederationCommand, media, media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand,
+	server, server::ServerCommand, user, user::UserCommand,
+};
+
+#[derive(Parser)]
+#[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
+pub(crate) enum AdminCommand {
+	#[command(subcommand)]
+	/// - Commands for managing appservices
+	Appservices(AppserviceCommand),
+
+	#[command(subcommand)]
+	/// - Commands for managing local users
+	Users(UserCommand),
+
+	#[command(subcommand)]
+	/// - Commands for managing rooms
+	Rooms(RoomCommand),
+
+	#[command(subcommand)]
+	/// - Commands for managing federation
+	Federation(FederationCommand),
+
+	#[command(subcommand)]
+	/// - Commands for managing the server
+	Server(ServerCommand),
+
+	#[command(subcommand)]
+	/// - Commands for managing media
+	Media(MediaCommand),
+
+	#[command(subcommand)]
+	/// - Commands for checking integrity
+	Check(CheckCommand),
+
+	#[command(subcommand)]
+	/// - Commands for debugging things
+	Debug(DebugCommand),
+
+	#[command(subcommand)]
+	/// - Low-level queries for database getters and iterators
+	Query(QueryCommand),
+}
+
+#[tracing::instrument(skip_all, name = "command")]
+pub(crate) async fn process(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
+	let reply_message_content = match command {
+		AdminCommand::Appservices(command) => appservice::process(command, body).await?,
+		AdminCommand::Media(command) => media::process(command, body).await?,
+		AdminCommand::Users(command) => user::process(command, body).await?,
+		AdminCommand::Rooms(command) => room::process(command, body).await?,
+		AdminCommand::Federation(command) => federation::process(command, body).await?,
+		AdminCommand::Server(command) => server::process(command, body).await?,
+		AdminCommand::Debug(command) => debug::process(command, body).await?,
+		AdminCommand::Query(command) => query::process(command, body).await?,
+		AdminCommand::Check(command) => check::process(command, body).await?,
+	};
+
+	Ok(reply_message_content)
+}
diff --git a/src/admin/handler.rs b/src/admin/handler.rs
index 409abc18f4a44f754a3819e29798269d878ff07e..46712077a788d99bcfa709d4c97a8dceec69d09d 100644
--- a/src/admin/handler.rs
+++ b/src/admin/handler.rs
@@ -11,64 +11,16 @@
 	OwnedEventId,
 };
 
-extern crate conduit_service as service;
-
 use conduit::{utils::string::common_prefix, Result};
-pub(crate) use service::admin::Command;
-use service::admin::{CommandOutput, CommandResult, HandlerResult};
+use service::admin::{Command, CommandOutput, CommandResult, HandlerResult};
 
-use crate::{
-	appservice, appservice::AppserviceCommand, check, check::CheckCommand, debug, debug::DebugCommand, federation,
-	federation::FederationCommand, media, media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand,
-	server, server::ServerCommand, services, user, user::UserCommand,
-};
-pub(crate) const PAGE_SIZE: usize = 100;
-
-#[derive(Parser)]
-#[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
-pub(crate) enum AdminCommand {
-	#[command(subcommand)]
-	/// - Commands for managing appservices
-	Appservices(AppserviceCommand),
-
-	#[command(subcommand)]
-	/// - Commands for managing local users
-	Users(UserCommand),
-
-	#[command(subcommand)]
-	/// - Commands for managing rooms
-	Rooms(RoomCommand),
-
-	#[command(subcommand)]
-	/// - Commands for managing federation
-	Federation(FederationCommand),
-
-	#[command(subcommand)]
-	/// - Commands for managing the server
-	Server(ServerCommand),
-
-	#[command(subcommand)]
-	/// - Commands for managing media
-	Media(MediaCommand),
-
-	#[command(subcommand)]
-	/// - Commands for checking integrity
-	Check(CheckCommand),
-
-	#[command(subcommand)]
-	/// - Commands for debugging things
-	Debug(DebugCommand),
-
-	#[command(subcommand)]
-	/// - Low-level queries for database getters and iterators
-	Query(QueryCommand),
-}
+use crate::{admin, admin::AdminCommand, services};
 
 #[must_use]
-pub(crate) fn handle(command: Command) -> HandlerResult { Box::pin(handle_command(command)) }
+pub(super) fn handle(command: Command) -> HandlerResult { Box::pin(handle_command(command)) }
 
 #[must_use]
-pub(crate) fn complete(line: &str) -> String { complete_admin_command(AdminCommand::command(), line) }
+pub(super) fn complete(line: &str) -> String { complete_command(AdminCommand::command(), line) }
 
 #[tracing::instrument(skip_all, name = "admin")]
 async fn handle_command(command: Command) -> CommandResult {
@@ -80,7 +32,7 @@ async fn handle_command(command: Command) -> CommandResult {
 }
 
 async fn process_command(command: &Command) -> CommandOutput {
-	process_admin_message(&command.command)
+	process(&command.command)
 		.await
 		.and_then(|content| reply(content, command.reply_id.clone()))
 }
@@ -104,11 +56,11 @@ fn reply(mut content: RoomMessageEventContent, reply_id: Option<OwnedEventId>) -
 }
 
 // Parse and process a message from the admin room
-async fn process_admin_message(msg: &str) -> CommandOutput {
+async fn process(msg: &str) -> CommandOutput {
 	let mut lines = msg.lines().filter(|l| !l.trim().is_empty());
 	let command = lines.next().expect("each string has at least one line");
 	let body = lines.collect::<Vec<_>>();
-	let parsed = match parse_admin_command(command) {
+	let parsed = match parse_command(command) {
 		Ok(parsed) => parsed,
 		Err(error) => {
 			let server_name = services().globals.server_name();
@@ -118,7 +70,7 @@ async fn process_admin_message(msg: &str) -> CommandOutput {
 	};
 
 	let timer = Instant::now();
-	let result = process_admin_command(parsed, body).await;
+	let result = admin::process(parsed, body).await;
 	let elapsed = timer.elapsed();
 	conduit::debug!(?command, ok = result.is_ok(), "command processed in {elapsed:?}");
 	match result {
@@ -129,31 +81,14 @@ async fn process_admin_message(msg: &str) -> CommandOutput {
 	}
 }
 
-#[tracing::instrument(skip_all, name = "command")]
-async fn process_admin_command(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
-	let reply_message_content = match command {
-		AdminCommand::Appservices(command) => appservice::process(command, body).await?,
-		AdminCommand::Media(command) => media::process(command, body).await?,
-		AdminCommand::Users(command) => user::process(command, body).await?,
-		AdminCommand::Rooms(command) => room::process(command, body).await?,
-		AdminCommand::Federation(command) => federation::process(command, body).await?,
-		AdminCommand::Server(command) => server::process(command, body).await?,
-		AdminCommand::Debug(command) => debug::process(command, body).await?,
-		AdminCommand::Query(command) => query::process(command, body).await?,
-		AdminCommand::Check(command) => check::process(command, body).await?,
-	};
-
-	Ok(reply_message_content)
-}
-
 // Parse chat messages from the admin room into an AdminCommand object
-fn parse_admin_command(command_line: &str) -> Result<AdminCommand, String> {
-	let argv = parse_command_line(command_line);
+fn parse_command(command_line: &str) -> Result<AdminCommand, String> {
+	let argv = parse_line(command_line);
 	AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
 }
 
-fn complete_admin_command(mut cmd: clap::Command, line: &str) -> String {
-	let argv = parse_command_line(line);
+fn complete_command(mut cmd: clap::Command, line: &str) -> String {
+	let argv = parse_line(line);
 	let mut ret = Vec::<String>::with_capacity(argv.len().saturating_add(1));
 
 	'token: for token in argv.into_iter().skip(1) {
@@ -196,7 +131,7 @@ fn complete_admin_command(mut cmd: clap::Command, line: &str) -> String {
 }
 
 // Parse chat messages from the admin room into an AdminCommand object
-fn parse_command_line(command_line: &str) -> Vec<String> {
+fn parse_line(command_line: &str) -> Vec<String> {
 	let mut argv = command_line
 		.split_whitespace()
 		.map(str::to_owned)
diff --git a/src/admin/mod.rs b/src/admin/mod.rs
index b183f3f6483652d2b391e6a88394498688f06124..ff2aefd5c8fbb666b78d6990f442c597183d7395 100644
--- a/src/admin/mod.rs
+++ b/src/admin/mod.rs
@@ -1,18 +1,20 @@
 #![recursion_limit = "168"]
 #![allow(clippy::wildcard_imports)]
 
+pub(crate) mod admin;
+pub(crate) mod handler;
+mod tests;
+pub(crate) mod utils;
+
 pub(crate) mod appservice;
 pub(crate) mod check;
 pub(crate) mod debug;
 pub(crate) mod federation;
-pub(crate) mod handler;
 pub(crate) mod media;
 pub(crate) mod query;
 pub(crate) mod room;
 pub(crate) mod server;
-mod tests;
 pub(crate) mod user;
-pub(crate) mod utils;
 
 extern crate conduit_api as api;
 extern crate conduit_core as conduit;
@@ -23,6 +25,8 @@
 
 pub(crate) use crate::utils::{escape_html, get_room_info};
 
+pub(crate) const PAGE_SIZE: usize = 100;
+
 mod_ctor! {}
 mod_dtor! {}
 
diff --git a/src/admin/room/room_commands.rs b/src/admin/room/room_commands.rs
index 1a387c7e1127004e398b7072a36403d4da9ea2af..cf0f3ddbd6fc35b318c8baf2b63ac4a6046390f6 100644
--- a/src/admin/room/room_commands.rs
+++ b/src/admin/room/room_commands.rs
@@ -2,7 +2,7 @@
 
 use ruma::events::room::message::RoomMessageEventContent;
 
-use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
+use crate::{escape_html, get_room_info, services, Result, PAGE_SIZE};
 
 pub(super) async fn list(
 	_body: Vec<&str>, page: Option<usize>, exclude_disabled: bool, exclude_banned: bool,
diff --git a/src/admin/room/room_directory_commands.rs b/src/admin/room/room_directory_commands.rs
index c9b4eb9e0ddcc72d7c6607356df42610298a7008..912e970c6fb894230e2886bd237c77ebdb857dbd 100644
--- a/src/admin/room/room_directory_commands.rs
+++ b/src/admin/room/room_directory_commands.rs
@@ -3,7 +3,7 @@
 use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
 
 use super::RoomDirectoryCommand;
-use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
+use crate::{escape_html, get_room_info, services, Result, PAGE_SIZE};
 
 pub(super) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
 	match command {
diff --git a/src/admin/tests.rs b/src/admin/tests.rs
index 69ccd896c7643fc2aef79c0e969568ee37d1f5fc..296d4888784847fccc38e66945fae5f35db6fd22 100644
--- a/src/admin/tests.rs
+++ b/src/admin/tests.rs
@@ -12,7 +12,7 @@
 fn get_help_inner(input: &str) {
 	use clap::Parser;
 
-	use crate::handler::AdminCommand;
+	use crate::admin::AdminCommand;
 
 	let Err(error) = AdminCommand::try_parse_from(["argv[0] doesn't matter", input]) else {
 		panic!("no error!");