diff --git a/src/app.rs b/src/app.rs
index 49c3d70..0443389 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -87,6 +87,8 @@ pub enum Error {
 
 	#[error("Could not deserialize configuration: `{0}`")]
 	ConfigDeserialization(toml::de::Error),
+	#[error("Could not serialize configuration: `{0}`")]
+	ConfigSerialization(toml::ser::Error),
 	#[error("Could not deserialize collection")]
 	IndexDeserializationError,
 	#[error("Could not serialize collection")]
diff --git a/src/app/config.rs b/src/app/config.rs
index c7adac9..37539fd 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -114,8 +114,32 @@ impl Manager {
 
 	#[cfg(test)]
 	pub async fn apply(&self, config: storage::Config) -> Result<(), Error> {
-		*self.config.write().await = config.try_into()?;
-		// TODO persistence
+		self.mutate_fallible(|c| {
+			*c = config.try_into()?;
+			Ok(())
+		})
+		.await
+	}
+
+	async fn mutate<F: FnOnce(&mut Config)>(&self, op: F) -> Result<(), Error> {
+		self.mutate_fallible(|c| {
+			op(c);
+			Ok(())
+		})
+		.await
+	}
+
+	async fn mutate_fallible<F: FnOnce(&mut Config) -> Result<(), Error>>(
+		&self,
+		op: F,
+	) -> Result<(), Error> {
+		let mut config = self.config.write().await;
+		op(&mut config)?;
+		let serialized = toml::ser::to_string_pretty::<storage::Config>(&config.clone().into())
+			.map_err(Error::ConfigSerialization)?;
+		tokio::fs::write(&self.config_file_path, serialized.as_bytes())
+			.await
+			.map_err(|e| Error::Io(self.config_file_path.clone(), e))?;
 		Ok(())
 	}
 
@@ -125,10 +149,11 @@ impl Manager {
 		Duration::from_secs(seconds)
 	}
 
-	pub async fn set_index_sleep_duration(&self, duration: Duration) {
-		let mut config = self.config.write().await;
-		config.reindex_every_n_seconds = Some(duration.as_secs());
-		// TODO persistence
+	pub async fn set_index_sleep_duration(&self, duration: Duration) -> Result<(), Error> {
+		self.mutate(|c| {
+			c.reindex_every_n_seconds = Some(duration.as_secs());
+		})
+		.await
 	}
 
 	pub async fn get_index_album_art_pattern(&self) -> Regex {
@@ -137,20 +162,22 @@ impl Manager {
 		pattern.unwrap_or_else(|| Regex::new("Folder.(jpeg|jpg|png)").unwrap())
 	}
 
-	pub async fn set_index_album_art_pattern(&self, regex: Regex) {
-		let mut config = self.config.write().await;
-		config.album_art_pattern = Some(regex);
-		// TODO persistence
+	pub async fn set_index_album_art_pattern(&self, regex: Regex) -> Result<(), Error> {
+		self.mutate(|c| {
+			c.album_art_pattern = Some(regex);
+		})
+		.await
 	}
 
 	pub async fn get_ddns_update_url(&self) -> Option<http::Uri> {
 		self.config.read().await.ddns_url.clone()
 	}
 
-	pub async fn set_ddns_update_url(&self, url: http::Uri) {
-		let mut config = self.config.write().await;
-		config.ddns_url = Some(url);
-		// TODO persistence
+	pub async fn set_ddns_update_url(&self, url: http::Uri) -> Result<(), Error> {
+		self.mutate(|c| {
+			c.ddns_url = Some(url);
+		})
+		.await
 	}
 
 	pub async fn get_users(&self) -> Vec<User> {
@@ -169,9 +196,8 @@ impl Manager {
 		password: &str,
 		admin: bool,
 	) -> Result<(), Error> {
-		let mut config = self.config.write().await;
-		config.create_user(username, password, admin)
-		// TODO persistence
+		self.mutate_fallible(|c| c.create_user(username, password, admin))
+			.await
 	}
 
 	pub async fn login(&self, username: &str, password: &str) -> Result<auth::Token, Error> {
@@ -180,15 +206,13 @@ impl Manager {
 	}
 
 	pub async fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
-		let mut config = self.config.write().await;
-		config.set_is_admin(username, is_admin)
-		// TODO persistence
+		self.mutate_fallible(|c| c.set_is_admin(username, is_admin))
+			.await
 	}
 
 	pub async fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
-		let mut config = self.config.write().await;
-		config.set_password(username, password)
-		// TODO persistence
+		self.mutate_fallible(|c| c.set_password(username, password))
+			.await
 	}
 
 	pub async fn authenticate(
@@ -200,10 +224,8 @@ impl Manager {
 		config.authenticate(auth_token, scope, &self.auth_secret)
 	}
 
-	pub async fn delete_user(&self, username: &str) {
-		let mut config = self.config.write().await;
-		config.delete_user(username);
-		// TODO persistence
+	pub async fn delete_user(&self, username: &str) -> Result<(), Error> {
+		self.mutate(|c| c.delete_user(username)).await
 	}
 
 	pub async fn get_mounts(&self) -> Vec<MountDir> {
@@ -220,15 +242,25 @@ impl Manager {
 	}
 
 	pub async fn set_mounts(&self, mount_dirs: Vec<storage::MountDir>) -> Result<(), Error> {
-		self.config.write().await.set_mounts(mount_dirs)
-		// TODO persistence
+		self.mutate_fallible(|c| c.set_mounts(mount_dirs)).await
 	}
 }
 
 #[cfg(test)]
 mod test {
+	use crate::app::test;
+	use crate::test_name;
+
 	use super::*;
 
+	#[tokio::test]
+	async fn blank_config_is_valid() {
+		let config_path = PathBuf::from_iter(["test-data", "blank.toml"]);
+		Manager::new(&config_path, auth::Secret([0; 32]))
+			.await
+			.unwrap();
+	}
+
 	#[tokio::test]
 	async fn can_read_config() {
 		let config_path = PathBuf::from_iter(["test-data", "config.toml"]);
@@ -257,4 +289,18 @@ mod test {
 		);
 		assert!(config.users[0].hashed_password.is_some());
 	}
+
+	#[tokio::test]
+	async fn can_write_config() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
+		ctx.config_manager
+			.create_user("Walter", "example_password", false)
+			.await
+			.unwrap();
+
+		let manager = Manager::new(&ctx.config_manager.config_file_path, auth::Secret([0; 32]))
+			.await
+			.unwrap();
+		assert!(manager.get_user("Walter").await.is_ok());
+	}
 }
diff --git a/src/app/config/storage.rs b/src/app/config/storage.rs
index 73a503d..e6fd764 100644
--- a/src/app/config/storage.rs
+++ b/src/app/config/storage.rs
@@ -25,10 +25,10 @@ pub struct Config {
 	pub reindex_every_n_seconds: Option<u64>,
 	#[serde(skip_serializing_if = "Option::is_none")]
 	pub album_art_pattern: Option<String>,
-	#[serde(skip_serializing_if = "Vec::is_empty")]
+	#[serde(default, skip_serializing_if = "Vec::is_empty")]
 	pub mount_dirs: Vec<MountDir>,
 	#[serde(skip_serializing_if = "Option::is_none")]
 	pub ddns_url: Option<String>,
-	#[serde(skip_serializing_if = "Vec::is_empty")]
+	#[serde(default, skip_serializing_if = "Vec::is_empty")]
 	pub users: Vec<User>,
 }
diff --git a/src/app/config/user.rs b/src/app/config/user.rs
index 7091bc3..f4a7504 100644
--- a/src/app/config/user.rs
+++ b/src/app/config/user.rs
@@ -175,7 +175,7 @@ mod test {
 			.unwrap();
 		assert!(ctx.config_manager.get_user(TEST_USERNAME).await.is_ok());
 
-		ctx.config_manager.delete_user(TEST_USERNAME).await;
+		ctx.config_manager.delete_user(TEST_USERNAME).await.unwrap();
 		assert!(ctx.config_manager.get_user(TEST_USERNAME).await.is_err());
 	}
 
diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs
index 4725778..6611b92 100644
--- a/src/server/axum/api.rs
+++ b/src/server/axum/api.rs
@@ -126,20 +126,20 @@ async fn put_settings(
 		let Ok(regex) = Regex::new(&pattern) else {
 			return Err(APIError::InvalidAlbumArtPattern);
 		};
-		config_manager.set_index_album_art_pattern(regex).await;
+		config_manager.set_index_album_art_pattern(regex).await?;
 	}
 
 	if let Some(seconds) = new_settings.reindex_every_n_seconds {
 		config_manager
 			.set_index_sleep_duration(Duration::from_secs(seconds as u64))
-			.await;
+			.await?;
 	}
 
 	if let Some(url_string) = new_settings.ddns_update_url {
 		let Ok(uri) = http::Uri::try_from(url_string) else {
 			return Err(APIError::InvalidDDNSURL);
 		};
-		config_manager.set_ddns_update_url(uri).await;
+		config_manager.set_ddns_update_url(uri).await?;
 	}
 
 	Ok(())
@@ -239,7 +239,7 @@ async fn delete_user(
 			return Err(APIError::DeletingOwnAccount);
 		}
 	}
-	config_manager.delete_user(&name).await;
+	config_manager.delete_user(&name).await?;
 	Ok(())
 }
 
diff --git a/src/server/error.rs b/src/server/error.rs
index c959ebc..46379bb 100644
--- a/src/server/error.rs
+++ b/src/server/error.rs
@@ -124,6 +124,7 @@ impl From<app::Error> for APIError {
 			app::Error::IndexAlbumArtPatternInvalid => APIError::InvalidAlbumArtPattern,
 
 			app::Error::ConfigDeserialization(_) => APIError::Internal,
+			app::Error::ConfigSerialization(_) => APIError::Internal,
 			app::Error::IndexDeserializationError => APIError::Internal,
 			app::Error::IndexSerializationError => APIError::Internal,
 
diff --git a/test-data/blank.toml b/test-data/blank.toml
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/test-data/blank.toml
@@ -0,0 +1 @@
+