diff --git a/.env b/.env
new file mode 100644
index 0000000..1686fee
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+DATABASE_URL=sqlite:./src/db/schema.sqlite
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0ae085c..dc1c932 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ TestConfig.toml
 
 # Runtime artifacts
 *.sqlite
+**/*.sqlite-shm
+**/*.sqlite-wal
 polaris.log
 polaris.pid
 /thumbnails
diff --git a/Cargo.lock b/Cargo.lock
index bb09eb5..771a891 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -152,7 +152,7 @@ dependencies = [
  "futures-core",
  "futures-util",
  "mio",
- "socket2 0.5.3",
+ "socket2 0.5.7",
  "tokio",
  "tracing",
 ]
@@ -256,7 +256,7 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "smallvec",
- "socket2 0.5.3",
+ "socket2 0.5.7",
  "time 0.3.28",
  "url",
 ]
@@ -305,14 +305,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
 name = "ahash"
-version = "0.8.3"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
  "getrandom",
  "once_cell",
  "version_check",
+ "zerocopy",
 ]
 
 [[package]]
@@ -339,6 +340,12 @@ dependencies = [
  "alloc-no-stdlib",
 ]
 
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
 [[package]]
 name = "ape"
 version = "0.5.0"
@@ -354,6 +361,15 @@ version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
 
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -443,6 +459,9 @@ name = "bitflags"
 version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "block-buffer"
@@ -546,6 +565,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
 [[package]]
 name = "const_fn"
 version = "0.4.9"
@@ -605,6 +630,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crc32fast"
 version = "1.3.2"
@@ -649,14 +689,20 @@ dependencies = [
 ]
 
 [[package]]
-name = "crossbeam-utils"
-version = "0.8.16"
+name = "crossbeam-queue"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
 dependencies = [
- "cfg-if",
+ "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -682,6 +728,17 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.8"
@@ -701,50 +758,6 @@ dependencies = [
  "syn 1.0.109",
 ]
 
-[[package]]
-name = "diesel"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98235fdc2f355d330a8244184ab6b4b33c28679c0b4158f63138e51d6cf7e88"
-dependencies = [
- "diesel_derives",
- "libsqlite3-sys",
- "r2d2",
- "time 0.3.28",
-]
-
-[[package]]
-name = "diesel_derives"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e054665eaf6d97d1e7125512bb2d35d07c73ac86cc6920174cb42d1ab697a554"
-dependencies = [
- "diesel_table_macro_syntax",
- "proc-macro2",
- "quote",
- "syn 2.0.31",
-]
-
-[[package]]
-name = "diesel_migrations"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac"
-dependencies = [
- "diesel",
- "migrations_internals",
- "migrations_macros",
-]
-
-[[package]]
-name = "diesel_table_macro_syntax"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5"
-dependencies = [
- "syn 2.0.31",
-]
-
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -752,6 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
+ "const-oid",
  "crypto-common",
  "subtle",
 ]
@@ -762,11 +776,20 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
 
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
 [[package]]
 name = "either"
 version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "embed-resource"
@@ -796,6 +819,39 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
 [[package]]
 name = "fdeflate"
 version = "0.3.0"
@@ -821,6 +877,17 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin 0.9.8",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -842,11 +909,49 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
 
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
 [[package]]
 name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
 version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
 
 [[package]]
 name = "futures-macro"
@@ -861,9 +966,9 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.28"
+version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
 
 [[package]]
 name = "futures-task"
@@ -878,9 +983,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
 dependencies = [
  "futures-core",
+ "futures-io",
  "futures-macro",
  "futures-sink",
  "futures-task",
+ "memchr",
  "pin-project-lite",
  "pin-utils",
  "slab",
@@ -962,6 +1069,19 @@ name = "hashbrown"
 version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.0",
+]
 
 [[package]]
 name = "headers"
@@ -987,6 +1107,15 @@ dependencies = [
  "http",
 ]
 
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.3.2"
@@ -999,6 +1128,15 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
 [[package]]
 name = "hmac"
 version = "0.12.1"
@@ -1008,6 +1146,15 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "http"
 version = "0.2.9"
@@ -1152,6 +1299,9 @@ name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin 0.5.2",
+]
 
 [[package]]
 name = "lewton"
@@ -1171,16 +1321,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
 
 [[package]]
-name = "libsqlite3-sys"
-version = "0.26.0"
+name = "libm"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
 dependencies = [
  "cc",
  "pkg-config",
  "vcpkg",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
 [[package]]
 name = "local-channel"
 version = "0.1.3"
@@ -1221,6 +1383,16 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
 
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
 [[package]]
 name = "md5"
 version = "0.7.0"
@@ -1253,27 +1425,6 @@ dependencies = [
  "log",
 ]
 
-[[package]]
-name = "migrations_internals"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada"
-dependencies = [
- "serde",
- "toml 0.7.8",
-]
-
-[[package]]
-name = "migrations_macros"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08"
-dependencies = [
- "migrations_internals",
- "proc-macro2",
- "quote",
-]
-
 [[package]]
 name = "mime"
 version = "0.3.17"
@@ -1290,6 +1441,12 @@ dependencies = [
  "unicase",
 ]
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.7.1"
@@ -1302,14 +1459,14 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.8"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
  "log",
  "wasi",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1361,6 +1518,33 @@ dependencies = [
  "winapi-build",
 ]
 
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.45"
@@ -1371,6 +1555,17 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-iter"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-rational"
 version = "0.4.1"
@@ -1389,6 +1584,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -1471,9 +1667,9 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.3.5",
  "smallvec",
- "windows-targets",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -1505,6 +1701,15 @@ dependencies = [
  "sha2",
 ]
 
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.0"
@@ -1523,6 +1728,27 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.27"
@@ -1555,8 +1781,6 @@ dependencies = [
  "branca",
  "crossbeam-channel",
  "daemonize",
- "diesel",
- "diesel_migrations",
  "embed-resource",
  "fs_extra",
  "futures-util",
@@ -1566,7 +1790,6 @@ dependencies = [
  "id3",
  "image",
  "lewton",
- "libsqlite3-sys",
  "log",
  "metaflac",
  "mp3-duration",
@@ -1586,7 +1809,9 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "simplelog",
+ "sqlx",
  "thiserror",
+ "tokio",
  "toml 0.7.8",
  "ureq 2.7.1",
  "url",
@@ -1651,17 +1876,6 @@ dependencies = [
  "proc-macro2",
 ]
 
-[[package]]
-name = "r2d2"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
-dependencies = [
- "log",
- "parking_lot",
- "scheduled-thread-pool",
-]
-
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -1723,6 +1937,15 @@ dependencies = [
  "bitflags 1.3.2",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
 [[package]]
 name = "regex"
 version = "1.9.5"
@@ -1761,12 +1984,32 @@ dependencies = [
  "cc",
  "libc",
  "once_cell",
- "spin",
+ "spin 0.5.2",
  "untrusted",
  "web-sys",
  "winapi",
 ]
 
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.23"
@@ -1804,6 +2047,19 @@ dependencies = [
  "wrapped-vec",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.4.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "rustls"
 version = "0.19.1"
@@ -1855,15 +2111,6 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
-[[package]]
-name = "scheduled-thread-pool"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
-dependencies = [
- "parking_lot",
-]
-
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -2015,6 +2262,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
 [[package]]
 name = "simd-adler32"
 version = "0.3.7"
@@ -2059,12 +2316,12 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.3"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2073,6 +2330,228 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
+dependencies = [
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
+dependencies = [
+ "ahash",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap 2.0.0",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
+dependencies = [
+ "atoi",
+ "base64 0.21.3",
+ "bitflags 2.4.0",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1 0.10.5",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
+dependencies = [
+ "atoi",
+ "base64 0.21.3",
+ "bitflags 2.4.0",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "sqlx-core",
+ "tracing",
+ "url",
+ "urlencoding",
+]
+
 [[package]]
 name = "standback"
 version = "0.2.17"
@@ -2131,6 +2610,17 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
 
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
 [[package]]
 name = "subtle"
 version = "2.5.0"
@@ -2159,6 +2649,18 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -2273,19 +2775,43 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.32.0"
+version = "1.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
 dependencies = [
  "backtrace",
  "bytes",
  "libc",
  "mio",
+ "num_cpus",
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.5.3",
- "windows-sys",
+ "socket2 0.5.7",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.31",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
@@ -2354,9 +2880,21 @@ dependencies = [
  "cfg-if",
  "log",
  "pin-project-lite",
+ "tracing-attributes",
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.31",
+]
+
 [[package]]
 name = "tracing-core"
 version = "0.1.31"
@@ -2402,12 +2940,30 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-properties"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
 
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -2460,6 +3016,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -2498,6 +3060,12 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.87"
@@ -2596,6 +3164,16 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
 
+[[package]]
+name = "whoami"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
+dependencies = [
+ "redox_syscall 0.4.1",
+ "wasite",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -2639,7 +3217,16 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2648,13 +3235,29 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
@@ -2663,42 +3266,90 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
 [[package]]
 name = "winnow"
 version = "0.5.15"
@@ -2737,6 +3388,26 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.31",
+]
+
 [[package]]
 name = "zeroize"
 version = "1.6.0"
diff --git a/Cargo.toml b/Cargo.toml
index e74955f..4e21678 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,8 +6,6 @@ edition = "2021"
 build = "build.rs"
 
 [features]
-default = ["bundle-sqlite"]
-bundle-sqlite = ["libsqlite3-sys"]
 ui = ["native-windows-gui", "native-windows-derive"]
 
 [dependencies]
@@ -18,16 +16,11 @@ ape = "0.5"
 base64 = "0.21"
 branca = "0.10.1"
 crossbeam-channel = "0.5"
-diesel_migrations = { version = "2.0", features = ["sqlite"] }
 futures-util = { version = "0.3" }
 getopts = "0.2.21"
 http = "0.2.8"
 id3 = "1.7.0"
 lewton = "0.10.2"
-libsqlite3-sys = { version = "0.26", features = [
-	"bundled",
-	"bundled-windows",
-], optional = true }
 log = "0.4.17"
 metaflac = "0.2.5"
 mp3-duration = "0.1.10"
@@ -44,19 +37,16 @@ serde = { version = "1.0.147", features = ["derive"] }
 serde_derive = "1.0.147"
 serde_json = "1.0.87"
 simplelog = "0.12.0"
+sqlx = { version = "0.7.4", features = ["migrate", "runtime-tokio", "sqlite"] }
 thiserror = "1.0.37"
+tokio = { version = "1.38", features = ["macros", "rt-multi-thread"] }
 toml = "0.7"
 ureq = "2.7"
 url = "2.3"
 
-[dependencies.diesel]
-version = "2.0.2"
-default_features = false
-features = ["libsqlite3-sys", "r2d2", "sqlite"]
-
 [dependencies.image]
 version = "0.24.4"
-default_features = false
+default-features = false
 features = ["bmp", "gif", "jpeg", "png"]
 
 [target.'cfg(windows)'.dependencies]
@@ -81,3 +71,6 @@ winres = "0.1"
 actix-test = "0.1.0"
 headers = "0.3"
 fs_extra = "1.2.0"
+
+[profile.dev.package.sqlx-macros]
+opt-level = 3
diff --git a/diesel.toml b/diesel.toml
deleted file mode 100644
index 1e154a7..0000000
--- a/diesel.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-[print_schema]
-file = "src/db/schema.rs"
diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md
index 56043dc..fd236fe 100644
--- a/docs/MAINTENANCE.md
+++ b/docs/MAINTENANCE.md
@@ -8,8 +8,3 @@
 - Input a user-facing version name (eg: **0.13.0**)
 - Click the **Run workflow** button
 - After CI completes, move the release from Draft to Published
-
-## How to change the database schema
-
-- Add a new folder under `migrations` following the existing pattern
-- Run `update_db_schema.bat`
diff --git a/migrations/201706250006_init/down.sql b/migrations/201706250006_init/down.sql
deleted file mode 100644
index a99d36b..0000000
--- a/migrations/201706250006_init/down.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-DROP TABLE directories;
-DROP TABLE songs;
\ No newline at end of file
diff --git a/migrations/201706250006_init/up.sql b/migrations/201706250006_init/up.sql
deleted file mode 100644
index fee773f..0000000
--- a/migrations/201706250006_init/up.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-CREATE TABLE directories (
-	id INTEGER PRIMARY KEY NOT NULL,
-	path TEXT NOT NULL,
-	parent TEXT,
-	artist TEXT,
-	year INTEGER,
-	album TEXT,
-	artwork TEXT,
-	UNIQUE(path) ON CONFLICT REPLACE
-);
-
-CREATE TABLE songs (
-	id INTEGER PRIMARY KEY NOT NULL,
-	path TEXT NOT NULL,
-	parent TEXT NOT NULL,
-	track_number INTEGER,
-	disc_number INTEGER,
-	title TEXT,
-	artist TEXT,
-	album_artist TEXT,
-	year INTEGER,
-	album TEXT,
-	artwork TEXT, 
-	UNIQUE(path) ON CONFLICT REPLACE
-);
\ No newline at end of file
diff --git a/migrations/201706250228_directories_date_added/down.sql b/migrations/201706250228_directories_date_added/down.sql
deleted file mode 100644
index 3323036..0000000
--- a/migrations/201706250228_directories_date_added/down.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-CREATE TEMPORARY TABLE directories_backup(id, path, parent, artist, year, album, artwork);
-INSERT INTO directories_backup SELECT id, path, parent, artist, year, album, artwork FROM directories;
-DROP TABLE directories;
-CREATE TABLE directories (
-	id INTEGER PRIMARY KEY NOT NULL,
-	path TEXT NOT NULL,
-	parent TEXT,
-	artist TEXT,
-	year INTEGER,
-	album TEXT,
-	artwork TEXT,
-	UNIQUE(path) ON CONFLICT REPLACE
-);
-INSERT INTO directories SELECT * FROM directories_backup;
-DROP TABLE directories_backup;
\ No newline at end of file
diff --git a/migrations/201706250228_directories_date_added/up.sql b/migrations/201706250228_directories_date_added/up.sql
deleted file mode 100644
index 7552c4e..0000000
--- a/migrations/201706250228_directories_date_added/up.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE directories ADD COLUMN date_added INTEGER DEFAULT 0 NOT NULL;
\ No newline at end of file
diff --git a/migrations/201706272129_users_table/down.sql b/migrations/201706272129_users_table/down.sql
deleted file mode 100644
index cc1f647..0000000
--- a/migrations/201706272129_users_table/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE users;
diff --git a/migrations/201706272129_users_table/up.sql b/migrations/201706272129_users_table/up.sql
deleted file mode 100644
index fd43421..0000000
--- a/migrations/201706272129_users_table/up.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE users (
-	id INTEGER PRIMARY KEY NOT NULL,
-	name TEXT NOT NULL,
-	password_salt BLOB NOT NULL,
-	password_hash BLOB NOT NULL,
-	admin INTEGER NOT NULL,
-	UNIQUE(name)
-);
diff --git a/migrations/201706272304_misc_settings_table/down.sql b/migrations/201706272304_misc_settings_table/down.sql
deleted file mode 100644
index 4cab917..0000000
--- a/migrations/201706272304_misc_settings_table/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE misc_settings;
diff --git a/migrations/201706272304_misc_settings_table/up.sql b/migrations/201706272304_misc_settings_table/up.sql
deleted file mode 100644
index c748840..0000000
--- a/migrations/201706272304_misc_settings_table/up.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE TABLE misc_settings (
-	id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	auth_secret TEXT NOT NULL,
-	index_sleep_duration_seconds INTEGER NOT NULL,
-	index_album_art_pattern TEXT NOT NULL
-);
-INSERT INTO misc_settings (id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern) VALUES (0, hex(randomblob(64)), 1800, "Folder.(jpeg|jpg|png)");
diff --git a/migrations/201706272313_ddns_config_table/down.sql b/migrations/201706272313_ddns_config_table/down.sql
deleted file mode 100644
index 72002c0..0000000
--- a/migrations/201706272313_ddns_config_table/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE ddns_config;
diff --git a/migrations/201706272313_ddns_config_table/up.sql b/migrations/201706272313_ddns_config_table/up.sql
deleted file mode 100644
index 7fbe94f..0000000
--- a/migrations/201706272313_ddns_config_table/up.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-CREATE TABLE ddns_config (
-	id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	host TEXT NOT NULL,
-	username TEXT NOT NULL,
-	password TEXT NOT NULL
-);
-
-INSERT INTO ddns_config (id, host, username, password) VALUES (0, "", "", "");
\ No newline at end of file
diff --git a/migrations/201706272327_mount_points_table/down.sql b/migrations/201706272327_mount_points_table/down.sql
deleted file mode 100644
index a7ff670..0000000
--- a/migrations/201706272327_mount_points_table/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE mount_points;
diff --git a/migrations/201706272327_mount_points_table/up.sql b/migrations/201706272327_mount_points_table/up.sql
deleted file mode 100644
index 9c3d6f9..0000000
--- a/migrations/201706272327_mount_points_table/up.sql
+++ /dev/null
@@ -1,6 +0,0 @@
-CREATE TABLE mount_points (
-	id INTEGER PRIMARY KEY NOT NULL,
-	source TEXT NOT NULL,
-	name TEXT NOT NULL,
-	UNIQUE(name)
-);
diff --git a/migrations/201707091522_playlists_tables/down.sql b/migrations/201707091522_playlists_tables/down.sql
deleted file mode 100644
index 8060d04..0000000
--- a/migrations/201707091522_playlists_tables/down.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-DROP TABLE playlists;
-DROP TABLE playlist_songs;
diff --git a/migrations/201707091522_playlists_tables/up.sql b/migrations/201707091522_playlists_tables/up.sql
deleted file mode 100644
index 69dea8f..0000000
--- a/migrations/201707091522_playlists_tables/up.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-CREATE TABLE playlists (
-	id INTEGER PRIMARY KEY NOT NULL,
-	owner INTEGER NOT NULL,
-	name TEXT NOT NULL,
-	FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE,
-	UNIQUE(owner, name) ON CONFLICT REPLACE
-);
-
-CREATE TABLE playlist_songs (
-	id INTEGER PRIMARY KEY NOT NULL,
-	playlist INTEGER NOT NULL,
-	path TEXT NOT NULL,
-	ordering INTEGER NOT NULL,
-	FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
-	UNIQUE(playlist, ordering) ON CONFLICT REPLACE
-);
diff --git a/migrations/20170929203228_add_prefix_url/down.sql b/migrations/20170929203228_add_prefix_url/down.sql
deleted file mode 100644
index 33b8650..0000000
--- a/migrations/20170929203228_add_prefix_url/down.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern);
-INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings;
-DROP TABLE misc_settings;
-CREATE TABLE misc_settings (
-	   id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	   auth_secret TEXT NOT NULL,
-	   index_sleep_duration_seconds INTEGER NOT NULL,
-	   index_album_art_pattern TEXT NOT NULL
-);
-INSERT INTO misc_settings SELECT * FROM misc_settings_backup;
-DROP TABLE misc_settings_backup;
diff --git a/migrations/20170929203228_add_prefix_url/up.sql b/migrations/20170929203228_add_prefix_url/up.sql
deleted file mode 100644
index cc16d29..0000000
--- a/migrations/20170929203228_add_prefix_url/up.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT "";
diff --git a/migrations/20171015224223_add_song_duration/down.sql b/migrations/20171015224223_add_song_duration/down.sql
deleted file mode 100644
index 6f538da..0000000
--- a/migrations/20171015224223_add_song_duration/down.sql
+++ /dev/null
@@ -1,19 +0,0 @@
-CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork);
-INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork FROM songs;
-DROP TABLE songs;
-CREATE TABLE songs (
-	id INTEGER PRIMARY KEY NOT NULL,
-	path TEXT NOT NULL,
-	parent TEXT NOT NULL,
-	track_number INTEGER,
-	disc_number INTEGER,
-	title TEXT,
-	artist TEXT,
-	album_artist TEXT,
-	year INTEGER,
-	album TEXT,
-	artwork TEXT,
-	UNIQUE(path) ON CONFLICT REPLACE
-);
-INSERT INTO songs SELECT * FROM songs_backup;
-DROP TABLE songs_backup;
diff --git a/migrations/20171015224223_add_song_duration/up.sql b/migrations/20171015224223_add_song_duration/up.sql
deleted file mode 100644
index cea3ac5..0000000
--- a/migrations/20171015224223_add_song_duration/up.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE songs ADD COLUMN duration INTEGER;
diff --git a/migrations/20180303211100_add_last_fm_credentials/down.sql b/migrations/20180303211100_add_last_fm_credentials/down.sql
deleted file mode 100644
index c6fda4e..0000000
--- a/migrations/20180303211100_add_last_fm_credentials/down.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE TEMPORARY TABLE users_backup(id, name, password_salt, password_hash, admin);
-INSERT INTO users_backup SELECT id, name, password_salt, password_hash, admin FROM users;
-DROP TABLE users;
-CREATE TABLE users (
-	id INTEGER PRIMARY KEY NOT NULL,
-	name TEXT NOT NULL,
-	password_salt BLOB NOT NULL,
-	password_hash BLOB NOT NULL,
-	admin INTEGER NOT NULL,
-	UNIQUE(name)
-);
-INSERT INTO users SELECT * FROM users_backup;
-DROP TABLE users_backup;
diff --git a/migrations/20180303211100_add_last_fm_credentials/up.sql b/migrations/20180303211100_add_last_fm_credentials/up.sql
deleted file mode 100644
index 40cc2e1..0000000
--- a/migrations/20180303211100_add_last_fm_credentials/up.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE users ADD COLUMN lastfm_username TEXT;
-ALTER TABLE users ADD COLUMN lastfm_session_key TEXT;
diff --git a/migrations/2019-08-08-042731_blob_auth_secret/down.sql b/migrations/2019-08-08-042731_blob_auth_secret/down.sql
deleted file mode 100644
index 8dc48c9..0000000
--- a/migrations/2019-08-08-042731_blob_auth_secret/down.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url);
-INSERT INTO misc_settings_backup
-SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url
-FROM misc_settings;
-DROP TABLE misc_settings;
-CREATE TABLE misc_settings (
-	   id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	   auth_secret BLOB NOT NULL DEFAULT (hex(randomblob(32))),
-	   index_sleep_duration_seconds INTEGER NOT NULL,
-	   index_album_art_pattern TEXT NOT NULL,
-       prefix_url TEXT NOT NULL DEFAULT ""
-);
-INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url)
-SELECT * FROM misc_settings_backup;
-DROP TABLE misc_settings_backup;
diff --git a/migrations/2019-08-08-042731_blob_auth_secret/up.sql b/migrations/2019-08-08-042731_blob_auth_secret/up.sql
deleted file mode 100644
index 9f15588..0000000
--- a/migrations/2019-08-08-042731_blob_auth_secret/up.sql
+++ /dev/null
@@ -1,15 +0,0 @@
-CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url);
-INSERT INTO misc_settings_backup
-SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url
-FROM misc_settings;
-DROP TABLE misc_settings;
-CREATE TABLE misc_settings (
-	   id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	   auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
-	   index_sleep_duration_seconds INTEGER NOT NULL,
-	   index_album_art_pattern TEXT NOT NULL,
-       prefix_url TEXT NOT NULL DEFAULT ""
-);
-INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url)
-SELECT * FROM misc_settings_backup;
-DROP TABLE misc_settings_backup;
diff --git a/migrations/2019-09-28-231910_pbkdf2_simple/down.sql b/migrations/2019-09-28-231910_pbkdf2_simple/down.sql
deleted file mode 100644
index 573f83f..0000000
--- a/migrations/2019-09-28-231910_pbkdf2_simple/down.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-DROP TABLE users;
-CREATE TABLE users (
-	id INTEGER PRIMARY KEY NOT NULL,
-	name TEXT NOT NULL,
-	password_salt BLOB NOT NULL,
-	password_hash BLOB NOT NULL,
-	admin INTEGER NOT NULL,
-	lastfm_username TEXT,
-	lastfm_session_key TEXT,
-	UNIQUE(name)
-);
diff --git a/migrations/2019-09-28-231910_pbkdf2_simple/up.sql b/migrations/2019-09-28-231910_pbkdf2_simple/up.sql
deleted file mode 100644
index 99a4fe5..0000000
--- a/migrations/2019-09-28-231910_pbkdf2_simple/up.sql
+++ /dev/null
@@ -1,10 +0,0 @@
-DROP TABLE users;
-CREATE TABLE users (
-	id INTEGER PRIMARY KEY NOT NULL,
-	name TEXT NOT NULL,
-	password_hash TEXT NOT NULL,
-	admin INTEGER NOT NULL,
-	lastfm_username TEXT,
-	lastfm_session_key TEXT,
-	UNIQUE(name)
-);
diff --git a/migrations/2020-01-08-231420_add_theme/down.sql b/migrations/2020-01-08-231420_add_theme/down.sql
deleted file mode 100644
index 5bf69d8..0000000
--- a/migrations/2020-01-08-231420_add_theme/down.sql
+++ /dev/null
@@ -1,14 +0,0 @@
-CREATE TEMPORARY TABLE users_backup(id, name, password_hash, admin, lastfm_username, lastfm_session_key);
-INSERT INTO users_backup SELECT id, name, password_hash, admin, lastfm_username, lastfm_session_key FROM users;
-DROP TABLE users;
-CREATE TABLE users (
-	id INTEGER PRIMARY KEY NOT NULL,
-	name TEXT NOT NULL,
-	password_hash TEXT NOT NULL,
-	admin INTEGER NOT NULL,
-	lastfm_username TEXT,
-	lastfm_session_key TEXT,
-	UNIQUE(name)
-);
-INSERT INTO users SELECT * FROM users_backup;
-DROP TABLE users_backup;
diff --git a/migrations/2020-01-08-231420_add_theme/up.sql b/migrations/2020-01-08-231420_add_theme/up.sql
deleted file mode 100644
index 67addb5..0000000
--- a/migrations/2020-01-08-231420_add_theme/up.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE users ADD COLUMN web_theme_base TEXT;
-ALTER TABLE users ADD COLUMN web_theme_accent TEXT;
diff --git a/migrations/2020-11-25-174000_remove_prefix_url/down.sql b/migrations/2020-11-25-174000_remove_prefix_url/down.sql
deleted file mode 100644
index cc16d29..0000000
--- a/migrations/2020-11-25-174000_remove_prefix_url/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT "";
diff --git a/migrations/2020-11-25-174000_remove_prefix_url/up.sql b/migrations/2020-11-25-174000_remove_prefix_url/up.sql
deleted file mode 100644
index 3016c77..0000000
--- a/migrations/2020-11-25-174000_remove_prefix_url/up.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern);
-INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings;
-DROP TABLE misc_settings;
-CREATE TABLE misc_settings (
-	   id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	   auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
-	   index_sleep_duration_seconds INTEGER NOT NULL,
-	   index_album_art_pattern TEXT NOT NULL
-);
-INSERT INTO misc_settings SELECT * FROM misc_settings_backup;
-DROP TABLE misc_settings_backup;
diff --git a/migrations/2021-05-01-011426_add_lyricist/down.sql b/migrations/2021-05-01-011426_add_lyricist/down.sql
deleted file mode 100644
index 5a5b228..0000000
--- a/migrations/2021-05-01-011426_add_lyricist/down.sql
+++ /dev/null
@@ -1,20 +0,0 @@
-CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration);
-INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration FROM songs;
-DROP TABLE songs;
-CREATE TABLE songs (
-	id INTEGER PRIMARY KEY NOT NULL,
-	path TEXT NOT NULL,
-	parent TEXT NOT NULL,
-	track_number INTEGER,
-	disc_number INTEGER,
-	title TEXT,
-	artist TEXT,
-	album_artist TEXT,
-	year INTEGER,
-	album TEXT,
-	artwork TEXT,
-  duration INTEGER,
-	UNIQUE(path) ON CONFLICT REPLACE
-);
-INSERT INTO songs SELECT * FROM songs_backup;
-DROP TABLE songs_backup;
diff --git a/migrations/2021-05-01-011426_add_lyricist/up.sql b/migrations/2021-05-01-011426_add_lyricist/up.sql
deleted file mode 100644
index 16f7863..0000000
--- a/migrations/2021-05-01-011426_add_lyricist/up.sql
+++ /dev/null
@@ -1,4 +0,0 @@
-ALTER TABLE songs ADD COLUMN lyricist TEXT;
-ALTER TABLE songs ADD COLUMN composer TEXT;
-ALTER TABLE songs ADD COLUMN genre TEXT;
-ALTER TABLE songs ADD COLUMN label TEXT;
diff --git a/src/app.rs b/src/app.rs
index 93b9da4..d173b72 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -32,10 +32,8 @@ pub enum Error {
 #[derive(Clone)]
 pub struct App {
 	pub port: u16,
-	pub auth_secret: settings::AuthSecret,
 	pub web_dir_path: PathBuf,
 	pub swagger_dir_path: PathBuf,
-	pub db: DB,
 	pub index: index::Index,
 	pub config_manager: config::Manager,
 	pub ddns_manager: ddns::Manager,
@@ -48,8 +46,8 @@ pub struct App {
 }
 
 impl App {
-	pub fn new(port: u16, paths: Paths) -> Result<Self, Error> {
-		let db = DB::new(&paths.db_file_path)?;
+	pub async fn new(port: u16, paths: Paths) -> Result<Self, Error> {
+		let db = DB::new(&paths.db_file_path).await?;
 		fs::create_dir_all(&paths.web_dir_path)
 			.map_err(|e| Error::Io(paths.web_dir_path.clone(), e))?;
 		fs::create_dir_all(&paths.swagger_dir_path)
@@ -61,7 +59,7 @@ impl App {
 
 		let vfs_manager = vfs::Manager::new(db.clone());
 		let settings_manager = settings::Manager::new(db.clone());
-		let auth_secret = settings_manager.get_auth_secret()?;
+		let auth_secret = settings_manager.get_auth_secret().await?;
 		let ddns_manager = ddns::Manager::new(db.clone());
 		let user_manager = user::Manager::new(db.clone(), auth_secret);
 		let index = index::Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
@@ -77,14 +75,11 @@ impl App {
 
 		if let Some(config_path) = paths.config_file_path {
 			let config = config::Config::from_path(&config_path)?;
-			config_manager.apply(&config)?;
+			config_manager.apply(&config).await?;
 		}
 
-		let auth_secret = settings_manager.get_auth_secret()?;
-
 		Ok(Self {
 			port,
-			auth_secret,
 			web_dir_path: paths.web_dir_path,
 			swagger_dir_path: paths.swagger_dir_path,
 			index,
@@ -96,7 +91,6 @@ impl App {
 			thumbnail_manager,
 			user_manager,
 			vfs_manager,
-			db,
 		})
 	}
 }
diff --git a/src/app/config.rs b/src/app/config.rs
index c20c608..d527ee4 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -64,28 +64,28 @@ impl Manager {
 		}
 	}
 
-	pub fn apply(&self, config: &Config) -> Result<(), Error> {
+	pub async fn apply(&self, config: &Config) -> Result<(), Error> {
 		if let Some(new_settings) = &config.settings {
-			self.settings_manager.amend(new_settings)?;
+			self.settings_manager.amend(new_settings).await?;
 		}
 
 		if let Some(mount_dirs) = &config.mount_dirs {
-			self.vfs_manager.set_mount_dirs(mount_dirs)?;
+			self.vfs_manager.set_mount_dirs(mount_dirs).await?;
 		}
 
 		if let Some(ddns_config) = &config.ydns {
-			self.ddns_manager.set_config(ddns_config)?;
+			self.ddns_manager.set_config(ddns_config).await?;
 		}
 
 		if let Some(ref users) = config.users {
-			let old_users: Vec<user::User> = self.user_manager.list()?;
+			let old_users: Vec<user::User> = self.user_manager.list().await?;
 
 			// Delete users that are not in new list
 			for old_user in old_users
 				.iter()
 				.filter(|old_user| !users.iter().any(|u| u.name == old_user.name))
 			{
-				self.user_manager.delete(&old_user.name)?;
+				self.user_manager.delete(&old_user.name).await?;
 			}
 
 			// Insert new users
@@ -93,13 +93,17 @@ impl Manager {
 				.iter()
 				.filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
 			{
-				self.user_manager.create(new_user)?;
+				self.user_manager.create(new_user).await?;
 			}
 
 			// Update users
 			for user in users {
-				self.user_manager.set_password(&user.name, &user.password)?;
-				self.user_manager.set_is_admin(&user.name, user.admin)?;
+				self.user_manager
+					.set_password(&user.name, &user.password)
+					.await?;
+				self.user_manager
+					.set_is_admin(&user.name, user.admin)
+					.await?;
 			}
 		}
 
@@ -114,9 +118,9 @@ mod test {
 	use crate::app::test;
 	use crate::test_name;
 
-	#[test]
-	fn apply_saves_misc_settings() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn apply_saves_misc_settings() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_config = Config {
 			settings: Some(settings::NewSettings {
 				album_art_pattern: Some("🖼️\\.jpg".into()),
@@ -125,8 +129,8 @@ mod test {
 			..Default::default()
 		};
 
-		ctx.config_manager.apply(&new_config).unwrap();
-		let settings = ctx.settings_manager.read().unwrap();
+		ctx.config_manager.apply(&new_config).await.unwrap();
+		let settings = ctx.settings_manager.read().await.unwrap();
 		let new_settings = new_config.settings.unwrap();
 		assert_eq!(
 			settings.index_album_art_pattern,
@@ -138,9 +142,9 @@ mod test {
 		);
 	}
 
-	#[test]
-	fn apply_saves_mount_points() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn apply_saves_mount_points() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_config = Config {
 			mount_dirs: Some(vec![vfs::MountDir {
@@ -150,36 +154,37 @@ mod test {
 			..Default::default()
 		};
 
-		ctx.config_manager.apply(&new_config).unwrap();
-		let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap();
+		ctx.config_manager.apply(&new_config).await.unwrap();
+		let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().await.unwrap();
 		assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
 	}
 
-	#[test]
-	fn apply_saves_ddns_settings() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn apply_saves_ddns_settings() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_config = Config {
 			ydns: Some(ddns::Config {
-				host: "🐸🐸🐸.ydns.eu".into(),
-				username: "kfr🐸g".into(),
-				password: "tasty🐞".into(),
+				ddns_host: "🐸🐸🐸.ydns.eu".into(),
+				ddns_username: "kfr🐸g".into(),
+				ddns_password: "tasty🐞".into(),
 			}),
 			..Default::default()
 		};
 
-		ctx.config_manager.apply(&new_config).unwrap();
-		let actual_ddns = ctx.ddns_manager.config().unwrap();
+		ctx.config_manager.apply(&new_config).await.unwrap();
+		let actual_ddns = ctx.ddns_manager.config().await.unwrap();
 		assert_eq!(actual_ddns, new_config.ydns.unwrap());
 	}
 
-	#[test]
-	fn apply_can_toggle_admin() {
+	#[tokio::test]
+	async fn apply_can_toggle_admin() {
 		let ctx = test::ContextBuilder::new(test_name!())
 			.user("Walter", "Tasty🍖", true)
-			.build();
+			.build()
+			.await;
 
-		assert!(ctx.user_manager.list().unwrap()[0].is_admin());
+		assert!(ctx.user_manager.list().await.unwrap()[0].is_admin());
 
 		let new_config = Config {
 			users: Some(vec![user::NewUser {
@@ -189,7 +194,7 @@ mod test {
 			}]),
 			..Default::default()
 		};
-		ctx.config_manager.apply(&new_config).unwrap();
-		assert!(!ctx.user_manager.list().unwrap()[0].is_admin());
+		ctx.config_manager.apply(&new_config).await.unwrap();
+		assert!(!ctx.user_manager.list().await.unwrap()[0].is_admin());
 	}
 }
diff --git a/src/app/ddns.rs b/src/app/ddns.rs
index 980c67e..ba08099 100644
--- a/src/app/ddns.rs
+++ b/src/app/ddns.rs
@@ -1,11 +1,9 @@
 use base64::prelude::*;
-use diesel::prelude::*;
 use log::{debug, error};
 use serde::{Deserialize, Serialize};
-use std::thread;
-use std::time;
+use std::time::Duration;
 
-use crate::db::{self, ddns_config, DB};
+use crate::db::{self, DB};
 
 const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
 
@@ -18,15 +16,14 @@ pub enum Error {
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 }
 
-#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
-#[diesel(table_name = ddns_config)]
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub struct Config {
-	pub host: String,
-	pub username: String,
-	pub password: String,
+	pub ddns_host: String,
+	pub ddns_username: String,
+	pub ddns_password: String,
 }
 
 #[derive(Clone)]
@@ -39,15 +36,15 @@ impl Manager {
 		Self { db }
 	}
 
-	fn update_my_ip(&self) -> Result<(), Error> {
-		let config = self.config()?;
-		if config.host.is_empty() || config.username.is_empty() {
+	async fn update_my_ip(&self) -> Result<(), Error> {
+		let config = self.config().await?;
+		if config.ddns_host.is_empty() || config.ddns_username.is_empty() {
 			debug!("Skipping DDNS update because credentials are missing");
 			return Ok(());
 		}
 
-		let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
-		let credentials = format!("{}:{}", &config.username, &config.password);
+		let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.ddns_host);
+		let credentials = format!("{}:{}", &config.ddns_username, &config.ddns_password);
 		let response = ureq::get(full_url.as_str())
 			.set(
 				"Authorization",
@@ -62,40 +59,38 @@ impl Manager {
 		}
 	}
 
-	pub fn config(&self) -> Result<Config, Error> {
-		use crate::db::ddns_config::dsl::*;
-		let mut connection = self.db.connect()?;
-		Ok(ddns_config
-			.select((host, username, password))
-			.get_result(&mut connection)?)
+	pub async fn config(&self) -> Result<Config, Error> {
+		Ok(sqlx::query_as!(
+			Config,
+			"SELECT ddns_host, ddns_username, ddns_password FROM config"
+		)
+		.fetch_one(self.db.connect().await?.as_mut())
+		.await?)
 	}
 
-	pub fn set_config(&self, new_config: &Config) -> Result<(), Error> {
-		use crate::db::ddns_config::dsl::*;
-		let mut connection = self.db.connect()?;
-		diesel::update(ddns_config)
-			.set((
-				host.eq(&new_config.host),
-				username.eq(&new_config.username),
-				password.eq(&new_config.password),
-			))
-			.execute(&mut connection)?;
+	pub async fn set_config(&self, new_config: &Config) -> Result<(), Error> {
+		sqlx::query!(
+			"UPDATE config SET ddns_host = $1, ddns_username = $2, ddns_password = $3",
+			new_config.ddns_host,
+			new_config.ddns_username,
+			new_config.ddns_password
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 
 	pub fn begin_periodic_updates(&self) {
-		let cloned = self.clone();
-		std::thread::spawn(move || {
-			cloned.run();
+		tokio::spawn({
+			let ddns = self.clone();
+			async move {
+				loop {
+					if let Err(e) = ddns.update_my_ip().await {
+						error!("Dynamic DNS update error: {:?}", e);
+					}
+					tokio::time::sleep(Duration::from_secs(60 * 30)).await;
+				}
+			}
 		});
 	}
-
-	fn run(&self) {
-		loop {
-			if let Err(e) = self.update_my_ip() {
-				error!("Dynamic DNS update error: {:?}", e);
-			}
-			thread::sleep(time::Duration::from_secs(60 * 30));
-		}
-	}
 }
diff --git a/src/app/index.rs b/src/app/index.rs
index 0375ac2..b0b00cf 100644
--- a/src/app/index.rs
+++ b/src/app/index.rs
@@ -1,6 +1,7 @@
 use log::error;
-use std::sync::{Arc, Condvar, Mutex};
+use std::sync::Arc;
 use std::time::Duration;
+use tokio::sync::Notify;
 
 use crate::app::{settings, vfs};
 use crate::db::DB;
@@ -20,7 +21,7 @@ pub struct Index {
 	db: DB,
 	vfs_manager: vfs::Manager,
 	settings_manager: settings::Manager,
-	pending_reindex: Arc<(Mutex<bool>, Condvar)>,
+	pending_reindex: Arc<Notify>,
 }
 
 impl Index {
@@ -29,63 +30,45 @@ impl Index {
 			db,
 			vfs_manager,
 			settings_manager,
-
-			pending_reindex: Arc::new((
-				#[allow(clippy::mutex_atomic)]
-				Mutex::new(false),
-				Condvar::new(),
-			)),
+			pending_reindex: Arc::new(Notify::new()),
 		};
 
-		let commands_index = index.clone();
-		std::thread::spawn(move || {
-			commands_index.process_commands();
+		tokio::spawn({
+			let index = index.clone();
+			async move {
+				loop {
+					index.pending_reindex.notified().await;
+					if let Err(e) = index.update().await {
+						error!("Error while updating index: {}", e);
+					}
+				}
+			}
 		});
 
 		index
 	}
 
 	pub fn trigger_reindex(&self) {
-		let (lock, cvar) = &*self.pending_reindex;
-		let mut pending_reindex = lock.lock().unwrap();
-		*pending_reindex = true;
-		cvar.notify_one();
+		self.pending_reindex.notify_one();
 	}
 
 	pub fn begin_periodic_updates(&self) {
-		let auto_index = self.clone();
-		std::thread::spawn(move || {
-			auto_index.automatic_reindex();
+		tokio::spawn({
+			let index = self.clone();
+			async move {
+				loop {
+					index.trigger_reindex();
+					let sleep_duration = index
+						.settings_manager
+						.get_index_sleep_duration()
+						.await
+						.unwrap_or_else(|e| {
+							error!("Could not retrieve index sleep duration: {}", e);
+							Duration::from_secs(1800)
+						});
+					tokio::time::sleep(sleep_duration).await;
+				}
+			}
 		});
 	}
-
-	fn process_commands(&self) {
-		loop {
-			{
-				let (lock, cvar) = &*self.pending_reindex;
-				let mut pending = lock.lock().unwrap();
-				while !*pending {
-					pending = cvar.wait(pending).unwrap();
-				}
-				*pending = false;
-			}
-			if let Err(e) = self.update() {
-				error!("Error while updating index: {}", e);
-			}
-		}
-	}
-
-	fn automatic_reindex(&self) {
-		loop {
-			self.trigger_reindex();
-			let sleep_duration = self
-				.settings_manager
-				.get_index_sleep_duration()
-				.unwrap_or_else(|e| {
-					error!("Could not retrieve index sleep duration: {}", e);
-					Duration::from_secs(1800)
-				});
-			std::thread::sleep(sleep_duration);
-		}
-	}
 }
diff --git a/src/app/index/query.rs b/src/app/index/query.rs
index 5494635..ad921d0 100644
--- a/src/app/index/query.rs
+++ b/src/app/index/query.rs
@@ -1,15 +1,12 @@
-use diesel::dsl::sql;
-use diesel::prelude::*;
-use diesel::sql_types;
 use std::path::{Path, PathBuf};
 
 use super::*;
-use crate::db::{self, directories, songs};
+use crate::db;
 
 #[derive(thiserror::Error, Debug)]
 pub enum QueryError {
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error("Song was not found: `{0}`")]
@@ -18,25 +15,21 @@ pub enum QueryError {
 	Vfs(#[from] vfs::Error),
 }
 
-sql_function!(
-	#[aggregate]
-	fn random() -> Integer;
-);
-
 impl Index {
-	pub fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError>
+	pub async fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError>
 	where
 		P: AsRef<Path>,
 	{
 		let mut output = Vec::new();
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
 
 		if virtual_path.as_ref().components().count() == 0 {
 			// Browse top-level
-			let real_directories: Vec<Directory> = directories::table
-				.filter(directories::parent.is_null())
-				.load(&mut connection)?;
+			let real_directories =
+				sqlx::query_as!(Directory, "SELECT * FROM directories WHERE parent IS NULL")
+					.fetch_all(connection.as_mut())
+					.await?;
 			let virtual_directories = real_directories
 				.into_iter()
 				.filter_map(|d| d.virtualize(&vfs));
@@ -46,19 +39,28 @@ impl Index {
 			let real_path = vfs.virtual_to_real(virtual_path)?;
 			let real_path_string = real_path.as_path().to_string_lossy().into_owned();
 
-			let real_directories: Vec<Directory> = directories::table
-				.filter(directories::parent.eq(&real_path_string))
-				.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
-				.load(&mut connection)?;
+			let real_directories = sqlx::query_as!(
+				Directory,
+				"SELECT * FROM directories WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC",
+				real_path_string
+			)
+			.fetch_all(connection.as_mut())
+			.await?;
+
 			let virtual_directories = real_directories
 				.into_iter()
 				.filter_map(|d| d.virtualize(&vfs));
+
 			output.extend(virtual_directories.map(CollectionFile::Directory));
 
-			let real_songs: Vec<Song> = songs::table
-				.filter(songs::parent.eq(&real_path_string))
-				.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
-				.load(&mut connection)?;
+			let real_songs = sqlx::query_as!(
+				Song,
+				"SELECT * FROM songs WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC",
+				real_path_string
+			)
+			.fetch_all(connection.as_mut())
+			.await?;
+
 			let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
 			output.extend(virtual_songs.map(CollectionFile::Song));
 		}
@@ -66,76 +68,88 @@ impl Index {
 		Ok(output)
 	}
 
-	pub fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError>
+	pub async fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError>
 	where
 		P: AsRef<Path>,
 	{
-		use self::songs::dsl::*;
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
 
-		let real_songs: Vec<Song> = if virtual_path.as_ref().parent().is_some() {
+		let real_songs = if virtual_path.as_ref().parent().is_some() {
 			let real_path = vfs.virtual_to_real(virtual_path)?;
 			let song_path_filter = {
 				let mut path_buf = real_path;
 				path_buf.push("%");
 				path_buf.as_path().to_string_lossy().into_owned()
 			};
-			songs
-				.filter(path.like(&song_path_filter))
-				.order(path)
-				.load(&mut connection)?
+			sqlx::query_as!(
+				Song,
+				"SELECT * FROM songs WHERE path LIKE $1 ORDER BY path COLLATE NOCASE ASC",
+				song_path_filter
+			)
+			.fetch_all(connection.as_mut())
+			.await?
 		} else {
-			songs.order(path).load(&mut connection)?
+			sqlx::query_as!(Song, "SELECT * FROM songs ORDER BY path COLLATE NOCASE ASC")
+				.fetch_all(connection.as_mut())
+				.await?
 		};
 
 		let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
 		Ok(virtual_songs.collect::<Vec<_>>())
 	}
 
-	pub fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
-		use self::directories::dsl::*;
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
-		let real_directories: Vec<Directory> = directories
-			.filter(album.is_not_null())
-			.limit(count)
-			.order(random())
-			.load(&mut connection)?;
+	pub async fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
+
+		let real_directories = sqlx::query_as!(
+			Directory,
+			"SELECT * FROM directories WHERE album IS NOT NULL ORDER BY RANDOM() DESC LIMIT $1",
+			count
+		)
+		.fetch_all(connection.as_mut())
+		.await?;
+
 		let virtual_directories = real_directories
 			.into_iter()
 			.filter_map(|d| d.virtualize(&vfs));
 		Ok(virtual_directories.collect::<Vec<_>>())
 	}
 
-	pub fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
-		use self::directories::dsl::*;
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
-		let real_directories: Vec<Directory> = directories
-			.filter(album.is_not_null())
-			.order(date_added.desc())
-			.limit(count)
-			.load(&mut connection)?;
+	pub async fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
+
+		let real_directories = sqlx::query_as!(
+			Directory,
+			"SELECT * FROM directories WHERE album IS NOT NULL ORDER BY date_added DESC LIMIT $1",
+			count
+		)
+		.fetch_all(connection.as_mut())
+		.await?;
+
 		let virtual_directories = real_directories
 			.into_iter()
 			.filter_map(|d| d.virtualize(&vfs));
 		Ok(virtual_directories.collect::<Vec<_>>())
 	}
 
-	pub fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> {
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
+	pub async fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> {
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
 		let like_test = format!("%{}%", query);
 		let mut output = Vec::new();
 
 		// Find dirs with matching path and parent not matching
 		{
-			use self::directories::dsl::*;
-			let real_directories: Vec<Directory> = directories
-				.filter(path.like(&like_test))
-				.filter(parent.not_like(&like_test))
-				.load(&mut connection)?;
+			let real_directories = sqlx::query_as!(
+				Directory,
+				"SELECT * FROM directories WHERE path LIKE $1 AND parent NOT LIKE $1",
+				like_test
+			)
+			.fetch_all(connection.as_mut())
+			.await?;
 
 			let virtual_directories = real_directories
 				.into_iter()
@@ -146,17 +160,22 @@ impl Index {
 
 		// Find songs with matching title/album/artist and non-matching parent
 		{
-			use self::songs::dsl::*;
-			let real_songs: Vec<Song> = songs
-				.filter(
-					path.like(&like_test)
-						.or(title.like(&like_test))
-						.or(album.like(&like_test))
-						.or(artist.like(&like_test))
-						.or(album_artist.like(&like_test)),
-				)
-				.filter(parent.not_like(&like_test))
-				.load(&mut connection)?;
+			let real_songs = sqlx::query_as!(
+				Song,
+				r#"
+				SELECT * FROM songs
+				WHERE	(	path LIKE $1
+						OR	title LIKE $1
+						OR album LIKE $1
+						OR artist LIKE $1
+						OR album_artist LIKE $1
+						)
+					AND parent NOT LIKE $1
+				"#,
+				like_test
+			)
+			.fetch_all(connection.as_mut())
+			.await?;
 
 			let virtual_songs = real_songs.into_iter().filter_map(|d| d.virtualize(&vfs));
 
@@ -166,17 +185,20 @@ impl Index {
 		Ok(output)
 	}
 
-	pub fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> {
-		let vfs = self.vfs_manager.get_vfs()?;
-		let mut connection = self.db.connect()?;
+	pub async fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> {
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let mut connection = self.db.connect().await?;
 
 		let real_path = vfs.virtual_to_real(virtual_path)?;
 		let real_path_string = real_path.as_path().to_string_lossy();
 
-		use self::songs::dsl::*;
-		let real_song: Song = songs
-			.filter(path.eq(real_path_string))
-			.get_result(&mut connection)?;
+		let real_song = sqlx::query_as!(
+			Song,
+			"SELECT * FROM songs WHERE path = $1",
+			real_path_string
+		)
+		.fetch_one(connection.as_mut())
+		.await?;
 
 		match real_song.virtualize(&vfs) {
 			Some(s) => Ok(s),
diff --git a/src/app/index/test.rs b/src/app/index/test.rs
index 7ae7e87..a03b1b8 100644
--- a/src/app/index/test.rs
+++ b/src/app/index/test.rs
@@ -1,32 +1,37 @@
-use diesel::prelude::*;
 use std::default::Default;
 use std::path::{Path, PathBuf};
 
 use super::*;
 use crate::app::test;
-use crate::db::{directories, songs};
 use crate::test_name;
 
 const TEST_MOUNT_NAME: &str = "root";
 
-#[test]
-fn update_adds_new_content() {
+#[tokio::test]
+async fn update_adds_new_content() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
+		.build()
+		.await;
 
-	ctx.index.update().unwrap();
-	ctx.index.update().unwrap(); // Validates that subsequent updates don't run into conflicts
+	ctx.index.update().await.unwrap();
+	ctx.index.update().await.unwrap(); // Validates that subsequent updates don't run into conflicts
 
-	let mut connection = ctx.db.connect().unwrap();
-	let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
-	let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
+	let mut connection = ctx.db.connect().await.unwrap();
+	let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
+		.fetch_all(connection.as_mut())
+		.await
+		.unwrap();
+	let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
+		.fetch_all(connection.as_mut())
+		.await
+		.unwrap();
 	assert_eq!(all_directories.len(), 6);
 	assert_eq!(all_songs.len(), 13);
 }
 
-#[test]
-fn update_removes_missing_content() {
+#[tokio::test]
+async fn update_removes_missing_content() {
 	let builder = test::ContextBuilder::new(test_name!());
 
 	let original_collection_dir: PathBuf = ["test-data", "small-collection"].iter().collect();
@@ -42,39 +47,53 @@ fn update_removes_missing_content() {
 
 	let ctx = builder
 		.mount(TEST_MOUNT_NAME, test_collection_dir.to_str().unwrap())
-		.build();
+		.build()
+		.await;
 
-	ctx.index.update().unwrap();
+	ctx.index.update().await.unwrap();
 
 	{
-		let mut connection = ctx.db.connect().unwrap();
-		let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
-		let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
+		let mut connection = ctx.db.connect().await.unwrap();
+		let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
+			.fetch_all(connection.as_mut())
+			.await
+			.unwrap();
+		let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
+			.fetch_all(connection.as_mut())
+			.await
+			.unwrap();
 		assert_eq!(all_directories.len(), 6);
 		assert_eq!(all_songs.len(), 13);
 	}
 
 	let khemmis_directory = test_collection_dir.join("Khemmis");
 	std::fs::remove_dir_all(khemmis_directory).unwrap();
-	ctx.index.update().unwrap();
+	ctx.index.update().await.unwrap();
 	{
-		let mut connection = ctx.db.connect().unwrap();
-		let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
-		let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
+		let mut connection = ctx.db.connect().await.unwrap();
+		let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
+			.fetch_all(connection.as_mut())
+			.await
+			.unwrap();
+		let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
+			.fetch_all(connection.as_mut())
+			.await
+			.unwrap();
 		assert_eq!(all_directories.len(), 4);
 		assert_eq!(all_songs.len(), 8);
 	}
 }
 
-#[test]
-fn can_browse_top_level() {
+#[tokio::test]
+async fn can_browse_top_level() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
 
 	let root_path = Path::new(TEST_MOUNT_NAME);
-	let files = ctx.index.browse(Path::new("")).unwrap();
+	let files = ctx.index.browse(Path::new("")).await.unwrap();
 	assert_eq!(files.len(), 1);
 	match files[0] {
 		CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()),
@@ -82,17 +101,18 @@ fn can_browse_top_level() {
 	}
 }
 
-#[test]
-fn can_browse_directory() {
+#[tokio::test]
+async fn can_browse_directory() {
 	let khemmis_path: PathBuf = [TEST_MOUNT_NAME, "Khemmis"].iter().collect();
 	let tobokegao_path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
 
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
 
-	let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).unwrap();
+	let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).await.unwrap();
 
 	assert_eq!(files.len(), 2);
 	match files[0] {
@@ -106,73 +126,79 @@ fn can_browse_directory() {
 	}
 }
 
-#[test]
-fn can_flatten_root() {
+#[tokio::test]
+async fn can_flatten_root() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
-	let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
+	let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).await.unwrap();
 	assert_eq!(songs.len(), 13);
 	assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
 }
 
-#[test]
-fn can_flatten_directory() {
+#[tokio::test]
+async fn can_flatten_directory() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
 	let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
-	let songs = ctx.index.flatten(path).unwrap();
+	let songs = ctx.index.flatten(path).await.unwrap();
 	assert_eq!(songs.len(), 8);
 }
 
-#[test]
-fn can_flatten_directory_with_shared_prefix() {
+#[tokio::test]
+async fn can_flatten_directory_with_shared_prefix() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
 	let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)'
-	let songs = ctx.index.flatten(path).unwrap();
+	let songs = ctx.index.flatten(path).await.unwrap();
 	assert_eq!(songs.len(), 7);
 }
 
-#[test]
-fn can_get_random_albums() {
+#[tokio::test]
+async fn can_get_random_albums() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
-	let albums = ctx.index.get_random_albums(1).unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
+	let albums = ctx.index.get_random_albums(1).await.unwrap();
 	assert_eq!(albums.len(), 1);
 }
 
-#[test]
-fn can_get_recent_albums() {
+#[tokio::test]
+async fn can_get_recent_albums() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
-	ctx.index.update().unwrap();
-	let albums = ctx.index.get_recent_albums(2).unwrap();
+		.build()
+		.await;
+	ctx.index.update().await.unwrap();
+	let albums = ctx.index.get_recent_albums(2).await.unwrap();
 	assert_eq!(albums.len(), 2);
 	assert!(albums[0].date_added >= albums[1].date_added);
 }
 
-#[test]
-fn can_get_a_song() {
+#[tokio::test]
+async fn can_get_a_song() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
+		.build()
+		.await;
 
-	ctx.index.update().unwrap();
+	ctx.index.update().await.unwrap();
 
 	let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect();
 	let song_virtual_path = picnic_virtual_dir.join("05 - シャーベット (Sherbet).mp3");
 	let artwork_virtual_path = picnic_virtual_dir.join("Folder.png");
 
-	let song = ctx.index.get_song(&song_virtual_path).unwrap();
+	let song = ctx.index.get_song(&song_virtual_path).await.unwrap();
 	assert_eq!(song.path, song_virtual_path.to_string_lossy().as_ref());
 	assert_eq!(song.track_number, Some(5));
 	assert_eq!(song.disc_number, None);
@@ -187,29 +213,31 @@ fn can_get_a_song() {
 	);
 }
 
-#[test]
-fn indexes_embedded_artwork() {
+#[tokio::test]
+async fn indexes_embedded_artwork() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
+		.build()
+		.await;
 
-	ctx.index.update().unwrap();
+	ctx.index.update().await.unwrap();
 
 	let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect();
 	let song_virtual_path = picnic_virtual_dir.join("07 - なぜ (Why).mp3");
 
-	let song = ctx.index.get_song(&song_virtual_path).unwrap();
+	let song = ctx.index.get_song(&song_virtual_path).await.unwrap();
 	assert_eq!(
 		song.artwork,
 		Some(song_virtual_path.to_string_lossy().into_owned())
 	);
 }
 
-#[test]
-fn album_art_pattern_is_case_insensitive() {
+#[tokio::test]
+async fn album_art_pattern_is_case_insensitive() {
 	let ctx = test::ContextBuilder::new(test_name!())
 		.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-		.build();
+		.build()
+		.await;
 
 	let patterns = vec!["folder", "FOLDER"];
 
@@ -219,12 +247,13 @@ fn album_art_pattern_is_case_insensitive() {
 				album_art_pattern: Some(pattern.to_owned()),
 				..Default::default()
 			})
+			.await
 			.unwrap();
-		ctx.index.update().unwrap();
+		ctx.index.update().await.unwrap();
 
 		let hunted_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
 		let artwork_virtual_path = hunted_virtual_dir.join("Folder.jpg");
-		let song = &ctx.index.flatten(&hunted_virtual_dir).unwrap()[0];
+		let song = &ctx.index.flatten(&hunted_virtual_dir).await.unwrap()[0];
 		assert_eq!(
 			song.artwork,
 			Some(artwork_virtual_path.to_string_lossy().into_owned())
diff --git a/src/app/index/types.rs b/src/app/index/types.rs
index 427cf90..f0bf7c8 100644
--- a/src/app/index/types.rs
+++ b/src/app/index/types.rs
@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
 use std::path::Path;
 
 use crate::app::vfs::VFS;
-use crate::db::songs;
 
 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub enum CollectionFile {
@@ -10,23 +9,22 @@ pub enum CollectionFile {
 	Song(Song),
 }
 
-#[derive(Debug, PartialEq, Eq, Queryable, QueryableByName, Serialize, Deserialize)]
-#[diesel(table_name = songs)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Song {
 	#[serde(skip_serializing, skip_deserializing)]
-	id: i32,
+	pub id: i64,
 	pub path: String,
 	#[serde(skip_serializing, skip_deserializing)]
 	pub parent: String,
-	pub track_number: Option<i32>,
-	pub disc_number: Option<i32>,
+	pub track_number: Option<i64>,
+	pub disc_number: Option<i64>,
 	pub title: Option<String>,
 	pub artist: Option<String>,
 	pub album_artist: Option<String>,
-	pub year: Option<i32>,
+	pub year: Option<i64>,
 	pub album: Option<String>,
 	pub artwork: Option<String>,
-	pub duration: Option<i32>,
+	pub duration: Option<i64>,
 	pub lyricist: Option<String>,
 	pub composer: Option<String>,
 	pub genre: Option<String>,
@@ -49,18 +47,18 @@ impl Song {
 	}
 }
 
-#[derive(Debug, PartialEq, Eq, Queryable, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Directory {
 	#[serde(skip_serializing, skip_deserializing)]
-	id: i32,
+	pub id: i64,
 	pub path: String,
 	#[serde(skip_serializing, skip_deserializing)]
 	pub parent: Option<String>,
 	pub artist: Option<String>,
-	pub year: Option<i32>,
+	pub year: Option<i64>,
 	pub album: Option<String>,
 	pub artwork: Option<String>,
-	pub date_added: i32,
+	pub date_added: i64,
 }
 
 impl Directory {
diff --git a/src/app/index/update.rs b/src/app/index/update.rs
index aa916ff..8064227 100644
--- a/src/app/index/update.rs
+++ b/src/app/index/update.rs
@@ -20,7 +20,7 @@ pub enum Error {
 	#[error(transparent)]
 	IndexClean(#[from] cleaner::Error),
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error(transparent)]
@@ -28,46 +28,44 @@ pub enum Error {
 }
 
 impl Index {
-	pub fn update(&self) -> Result<(), Error> {
+	pub async fn update(&self) -> Result<(), Error> {
 		let start = time::Instant::now();
 		info!("Beginning library index update");
 
-		let album_art_pattern = self.settings_manager.get_index_album_art_pattern().ok();
+		let album_art_pattern = self
+			.settings_manager
+			.get_index_album_art_pattern()
+			.await
+			.ok();
 
 		let cleaner = Cleaner::new(self.db.clone(), self.vfs_manager.clone());
-		cleaner.clean()?;
+		cleaner.clean().await?;
 
-		let (insert_sender, insert_receiver) = crossbeam_channel::unbounded();
-		let inserter_db = self.db.clone();
-		let insertion_thread = std::thread::spawn(move || {
-			let mut inserter = Inserter::new(inserter_db, insert_receiver);
-			inserter.insert();
+		let (insert_sender, insert_receiver) = tokio::sync::mpsc::unbounded_channel();
+		let insertion = tokio::spawn({
+			let db = self.db.clone();
+			async {
+				let mut inserter = Inserter::new(db, insert_receiver);
+				inserter.insert().await;
+			}
 		});
 
 		let (collect_sender, collect_receiver) = crossbeam_channel::unbounded();
-		let collector_thread = std::thread::spawn(move || {
+		let collection = tokio::task::spawn_blocking(|| {
 			let collector = Collector::new(collect_receiver, insert_sender, album_art_pattern);
 			collector.collect();
 		});
 
-		let vfs = self.vfs_manager.get_vfs()?;
-		let traverser_thread = std::thread::spawn(move || {
+		let vfs = self.vfs_manager.get_vfs().await?;
+		let traversal = tokio::task::spawn_blocking(move || {
 			let mounts = vfs.mounts();
 			let traverser = Traverser::new(collect_sender);
 			traverser.traverse(mounts.iter().map(|p| p.source.clone()).collect());
 		});
 
-		if let Err(e) = traverser_thread.join() {
-			error!("Error joining on traverser thread: {:?}", e);
-		}
-
-		if let Err(e) = collector_thread.join() {
-			error!("Error joining on collector thread: {:?}", e);
-		}
-
-		if let Err(e) = insertion_thread.join() {
-			error!("Error joining on inserter thread: {:?}", e);
-		}
+		traversal.await.unwrap();
+		collection.await.unwrap();
+		insertion.await.unwrap();
 
 		info!(
 			"Library index update took {} seconds",
diff --git a/src/app/index/update/cleaner.rs b/src/app/index/update/cleaner.rs
index bc3bf69..6ce94f8 100644
--- a/src/app/index/update/cleaner.rs
+++ b/src/app/index/update/cleaner.rs
@@ -1,16 +1,16 @@
-use diesel::prelude::*;
 use rayon::prelude::*;
+use sqlx::{QueryBuilder, Sqlite};
 use std::path::Path;
 
 use crate::app::vfs;
-use crate::db::{self, directories, songs, DB};
+use crate::db::{self, DB};
 
 const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Deletions in each transaction
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error(transparent)]
@@ -29,19 +29,23 @@ impl Cleaner {
 		Self { db, vfs_manager }
 	}
 
-	pub fn clean(&self) -> Result<(), Error> {
-		let vfs = self.vfs_manager.get_vfs()?;
+	pub async fn clean(&self) -> Result<(), Error> {
+		let vfs = self.vfs_manager.get_vfs().await?;
 
-		let all_directories: Vec<String> = {
-			let mut connection = self.db.connect()?;
-			directories::table
-				.select(directories::path)
-				.load(&mut connection)?
-		};
+		let (all_directories, all_songs) = {
+			let mut connection = self.db.connect().await?;
 
-		let all_songs: Vec<String> = {
-			let mut connection = self.db.connect()?;
-			songs::table.select(songs::path).load(&mut connection)?
+			let directories = sqlx::query_scalar!("SELECT path FROM directories")
+				.fetch_all(connection.as_mut())
+				.await
+				.unwrap();
+
+			let songs = sqlx::query_scalar!("SELECT path FROM songs")
+				.fetch_all(connection.as_mut())
+				.await
+				.unwrap();
+
+			(directories, songs)
 		};
 
 		let list_missing_directories = || {
@@ -69,14 +73,26 @@ impl Cleaner {
 			thread_pool.join(list_missing_directories, list_missing_songs);
 
 		{
-			let mut connection = self.db.connect()?;
+			let mut connection = self.db.connect().await?;
+
 			for chunk in missing_directories[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) {
-				diesel::delete(directories::table.filter(directories::path.eq_any(chunk)))
-					.execute(&mut connection)?;
+				QueryBuilder::<Sqlite>::new("DELETE FROM directories WHERE path IN ")
+					.push_tuples(chunk, |mut b, path| {
+						b.push_bind(path);
+					})
+					.build()
+					.execute(connection.as_mut())
+					.await?;
 			}
+
 			for chunk in missing_songs[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) {
-				diesel::delete(songs::table.filter(songs::path.eq_any(chunk)))
-					.execute(&mut connection)?;
+				QueryBuilder::<Sqlite>::new("DELETE FROM songs WHERE path IN ")
+					.push_tuples(chunk, |mut b, path| {
+						b.push_bind(path);
+					})
+					.build()
+					.execute(connection.as_mut())
+					.await?;
 			}
 		}
 
diff --git a/src/app/index/update/collector.rs b/src/app/index/update/collector.rs
index c790086..89af45d 100644
--- a/src/app/index/update/collector.rs
+++ b/src/app/index/update/collector.rs
@@ -1,19 +1,18 @@
-use crossbeam_channel::{Receiver, Sender};
 use log::error;
 use regex::Regex;
 
 use super::*;
 
 pub struct Collector {
-	receiver: Receiver<traverser::Directory>,
-	sender: Sender<inserter::Item>,
+	receiver: crossbeam_channel::Receiver<traverser::Directory>,
+	sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>,
 	album_art_pattern: Option<Regex>,
 }
 
 impl Collector {
 	pub fn new(
-		receiver: Receiver<traverser::Directory>,
-		sender: Sender<inserter::Item>,
+		receiver: crossbeam_channel::Receiver<traverser::Directory>,
+		sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>,
 		album_art_pattern: Option<Regex>,
 	) -> Self {
 		Self {
diff --git a/src/app/index/update/inserter.rs b/src/app/index/update/inserter.rs
index 4c73e44..63de282 100644
--- a/src/app/index/update/inserter.rs
+++ b/src/app/index/update/inserter.rs
@@ -1,13 +1,11 @@
-use crossbeam_channel::Receiver;
-use diesel::prelude::*;
 use log::error;
+use sqlx::{QueryBuilder, Sqlite};
+use tokio::sync::mpsc::UnboundedReceiver;
 
-use crate::db::{directories, songs, DB};
+use crate::db::DB;
 
 const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
 
-#[derive(Debug, Insertable)]
-#[diesel(table_name = songs)]
 pub struct Song {
 	pub path: String,
 	pub parent: String,
@@ -26,8 +24,6 @@ pub struct Song {
 	pub label: Option<String>,
 }
 
-#[derive(Debug, Insertable)]
-#[diesel(table_name = directories)]
 pub struct Directory {
 	pub path: String,
 	pub parent: Option<String>,
@@ -44,14 +40,14 @@ pub enum Item {
 }
 
 pub struct Inserter {
-	receiver: Receiver<Item>,
+	receiver: UnboundedReceiver<Item>,
 	new_directories: Vec<Directory>,
 	new_songs: Vec<Song>,
 	db: DB,
 }
 
 impl Inserter {
-	pub fn new(db: DB, receiver: Receiver<Item>) -> Self {
+	pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self {
 		let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
 		let new_songs = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
 		Self {
@@ -62,63 +58,90 @@ impl Inserter {
 		}
 	}
 
-	pub fn insert(&mut self) {
-		while let Ok(item) = self.receiver.recv() {
-			self.insert_item(item);
+	pub async fn insert(&mut self) {
+		while let Some(item) = self.receiver.recv().await {
+			self.insert_item(item).await;
 		}
+		self.flush_directories().await;
+		self.flush_songs().await;
 	}
 
-	fn insert_item(&mut self, insert: Item) {
+	async fn insert_item(&mut self, insert: Item) {
 		match insert {
 			Item::Directory(d) => {
 				self.new_directories.push(d);
 				if self.new_directories.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE {
-					self.flush_directories();
+					self.flush_directories().await;
 				}
 			}
 			Item::Song(s) => {
 				self.new_songs.push(s);
 				if self.new_songs.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE {
-					self.flush_songs();
+					self.flush_songs().await;
 				}
 			}
 		};
 	}
 
-	fn flush_directories(&mut self) {
-		let res = self.db.connect().ok().and_then(|mut connection| {
-			diesel::insert_into(directories::table)
-				.values(&self.new_directories)
-				.execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822
-				.ok()
-		});
-		if res.is_none() {
-			error!("Could not insert new directories in database");
-		}
-		self.new_directories.clear();
+	async fn flush_directories(&mut self) {
+		let Ok(mut connection) = self.db.connect().await else {
+			error!("Could not acquire connection to insert new directories in database");
+			return;
+		};
+
+		let result = QueryBuilder::<Sqlite>::new(
+			"INSERT INTO directories(path, parent, artist, year, album, artwork, date_added) ",
+		)
+		.push_values(&self.new_directories, |mut b, directory| {
+			b.push_bind(&directory.path)
+				.push_bind(&directory.parent)
+				.push_bind(&directory.artist)
+				.push_bind(directory.year)
+				.push_bind(&directory.album)
+				.push_bind(&directory.artwork)
+				.push_bind(directory.date_added);
+		})
+		.build()
+		.execute(connection.as_mut())
+		.await;
+
+		match result {
+			Ok(_) => self.new_directories.clear(),
+			Err(_) => error!("Could not insert new directories in database"),
+		};
 	}
 
-	fn flush_songs(&mut self) {
-		let res = self.db.connect().ok().and_then(|mut connection| {
-			diesel::insert_into(songs::table)
-				.values(&self.new_songs)
-				.execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822
-				.ok()
-		});
-		if res.is_none() {
-			error!("Could not insert new songs in database");
-		}
-		self.new_songs.clear();
-	}
-}
+	async fn flush_songs(&mut self) {
+		let Ok(mut connection) = self.db.connect().await else {
+			error!("Could not acquire connection to insert new songs in database");
+			return;
+		};
 
-impl Drop for Inserter {
-	fn drop(&mut self) {
-		if !self.new_directories.is_empty() {
-			self.flush_directories();
-		}
-		if !self.new_songs.is_empty() {
-			self.flush_songs();
-		}
+		let result = QueryBuilder::<Sqlite>::new("INSERT INTO songs(path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration, lyricist, composer, genre, label) ")
+		.push_values(&self.new_songs, |mut b, song| {
+			b.push_bind(&song.path)
+				.push_bind(&song.parent)
+				.push_bind(song.track_number)
+				.push_bind(song.disc_number)
+				.push_bind(&song.title)
+				.push_bind(&song.artist)
+				.push_bind(&song.album_artist)
+				.push_bind(song.year)
+				.push_bind(&song.album)
+				.push_bind(&song.artwork)
+				.push_bind(song.duration)
+				.push_bind(&song.lyricist)
+				.push_bind(&song.composer)
+				.push_bind(&song.genre)
+				.push_bind(&song.label);
+		})
+		.build()
+		.execute(connection.as_mut())
+		.await;
+
+		match result {
+			Ok(_) => self.new_songs.clear(),
+			Err(_) => error!("Could not insert new songs in database"),
+		};
 	}
 }
diff --git a/src/app/lastfm.rs b/src/app/lastfm.rs
index 74f060f..0ace649 100644
--- a/src/app/lastfm.rs
+++ b/src/app/lastfm.rs
@@ -44,7 +44,7 @@ impl Manager {
 			.map_err(|e| e.into())
 	}
 
-	pub fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> {
+	pub async fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> {
 		let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
 		let auth_response = scrobbler
 			.authenticate_with_token(lastfm_token)
@@ -52,28 +52,30 @@ impl Manager {
 
 		self.user_manager
 			.lastfm_link(username, &auth_response.name, &auth_response.key)
+			.await
 			.map_err(|e| e.into())
 	}
 
-	pub fn unlink(&self, username: &str) -> Result<(), Error> {
+	pub async fn unlink(&self, username: &str) -> Result<(), Error> {
 		self.user_manager
 			.lastfm_unlink(username)
+			.await
 			.map_err(|e| e.into())
 	}
 
-	pub fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> {
+	pub async fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> {
 		let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
-		let scrobble = self.scrobble_from_path(track)?;
-		let auth_token = self.user_manager.get_lastfm_session_key(username)?;
+		let scrobble = self.scrobble_from_path(track).await?;
+		let auth_token = self.user_manager.get_lastfm_session_key(username).await?;
 		scrobbler.authenticate_with_session_key(&auth_token);
 		scrobbler.scrobble(&scrobble).map_err(Error::Scrobble)?;
 		Ok(())
 	}
 
-	pub fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> {
+	pub async fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> {
 		let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
-		let scrobble = self.scrobble_from_path(track)?;
-		let auth_token = self.user_manager.get_lastfm_session_key(username)?;
+		let scrobble = self.scrobble_from_path(track).await?;
+		let auth_token = self.user_manager.get_lastfm_session_key(username).await?;
 		scrobbler.authenticate_with_session_key(&auth_token);
 		scrobbler
 			.now_playing(&scrobble)
@@ -81,8 +83,8 @@ impl Manager {
 		Ok(())
 	}
 
-	fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
-		let song = self.index.get_song(track)?;
+	async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
+		let song = self.index.get_song(track).await?;
 		Ok(Scrobble::new(
 			song.artist.as_deref().unwrap_or(""),
 			song.title.as_deref().unwrap_or(""),
diff --git a/src/app/playlist.rs b/src/app/playlist.rs
index a2addfc..a0cb18a 100644
--- a/src/app/playlist.rs
+++ b/src/app/playlist.rs
@@ -1,17 +1,14 @@
 use core::clone::Clone;
-use diesel::prelude::*;
-use diesel::sql_types;
-use diesel::BelongingToDsl;
-use std::path::Path;
+use sqlx::{Acquire, QueryBuilder, Sqlite};
 
 use crate::app::index::Song;
 use crate::app::vfs;
-use crate::db::{self, playlist_songs, playlists, users, DB};
+use crate::db::{self, DB};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error("User not found")]
@@ -33,148 +30,138 @@ impl Manager {
 		Self { db, vfs_manager }
 	}
 
-	pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
-		let mut connection = self.db.connect()?;
+	pub async fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
+		let mut connection = self.db.connect().await?;
 
-		let user: User = {
-			use self::users::dsl::*;
-			users
-				.filter(name.eq(owner))
-				.select((id,))
-				.first(&mut connection)
-				.optional()?
-				.ok_or(Error::UserNotFound)?
-		};
+		let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
+			.fetch_optional(connection.as_mut())
+			.await?
+			.ok_or(Error::UserNotFound)?;
 
-		{
-			use self::playlists::dsl::*;
-			let found_playlists: Vec<String> = Playlist::belonging_to(&user)
-				.select(name)
-				.load(&mut connection)?;
-			Ok(found_playlists)
-		}
+		Ok(
+			sqlx::query_scalar!("SELECT name FROM playlists WHERE owner = $1", user_id)
+				.fetch_all(connection.as_mut())
+				.await?,
+		)
 	}
 
-	pub fn save_playlist(
+	pub async fn save_playlist(
 		&self,
 		playlist_name: &str,
 		owner: &str,
 		content: &[String],
 	) -> Result<(), Error> {
-		let new_playlist: NewPlaylist;
-		let playlist: Playlist;
-		let vfs = self.vfs_manager.get_vfs()?;
+		let vfs = self.vfs_manager.get_vfs().await?;
 
-		{
-			let mut connection = self.db.connect()?;
-
-			// Find owner
-			let user: User = {
-				use self::users::dsl::*;
-				users
-					.filter(name.eq(owner))
-					.select((id,))
-					.first(&mut connection)
-					.optional()?
-					.ok_or(Error::UserNotFound)?
-			};
-
-			// Create playlist
-			new_playlist = NewPlaylist {
-				name: playlist_name.into(),
-				owner: user.id,
-			};
-
-			diesel::insert_into(playlists::table)
-				.values(&new_playlist)
-				.execute(&mut connection)?;
-
-			playlist = {
-				use self::playlists::dsl::*;
-				playlists
-					.select((id, owner))
-					.filter(name.eq(playlist_name).and(owner.eq(user.id)))
-					.get_result(&mut connection)?
-			}
+		struct PlaylistSong {
+			path: String,
+			ordering: i64,
 		}
 
-		let mut new_songs: Vec<NewPlaylistSong> = Vec::with_capacity(content.len());
-
+		let mut new_songs: Vec<PlaylistSong> = Vec::with_capacity(content.len());
 		for (i, path) in content.iter().enumerate() {
-			let virtual_path = Path::new(&path);
 			if let Some(real_path) = vfs
-				.virtual_to_real(virtual_path)
+				.virtual_to_real(path)
 				.ok()
 				.and_then(|p| p.to_str().map(|s| s.to_owned()))
 			{
-				new_songs.push(NewPlaylistSong {
-					playlist: playlist.id,
+				new_songs.push(PlaylistSong {
 					path: real_path,
-					ordering: i as i32,
+					ordering: i as i64,
 				});
 			}
 		}
 
-		{
-			let mut connection = self.db.connect()?;
-			connection.transaction::<_, diesel::result::Error, _>(|connection| {
-				// Delete old content (if any)
-				let old_songs = PlaylistSong::belonging_to(&playlist);
-				diesel::delete(old_songs).execute(connection)?;
+		// Create playlist
+		let mut connection = self.db.connect().await?;
 
-				// Insert content
-				diesel::insert_into(playlist_songs::table)
-					.values(&new_songs)
-					.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
-				Ok(())
-			})?;
+		// Find owner
+		let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
+			.fetch_optional(connection.as_mut())
+			.await?
+			.ok_or(Error::UserNotFound)?;
+
+		sqlx::query!(
+			"INSERT INTO playlists (owner, name) VALUES($1, $2)",
+			user_id,
+			playlist_name
+		)
+		.execute(connection.as_mut())
+		.await?;
+
+		let playlist_id = sqlx::query_scalar!(
+			"SELECT id FROM playlists WHERE owner = $1 AND name = $2",
+			user_id,
+			playlist_name
+		)
+		.fetch_one(connection.as_mut())
+		.await?;
+
+		connection.acquire().await?;
+
+		sqlx::query!(
+			"DELETE FROM playlist_songs WHERE playlist = $1",
+			playlist_id
+		)
+		.execute(connection.as_mut())
+		.await?;
+
+		if !new_songs.is_empty() {
+			QueryBuilder::<Sqlite>::new("INSERT INTO playlist_songs (playlist, path, ordering) ")
+				.push_values(new_songs, |mut b, song| {
+					b.push_bind(playlist_id)
+						.push_bind(song.path)
+						.push_bind(song.ordering);
+				})
+				.build()
+				.execute(connection.as_mut())
+				.await?;
 		}
 
 		Ok(())
 	}
 
-	pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Vec<Song>, Error> {
-		let vfs = self.vfs_manager.get_vfs()?;
-		let songs: Vec<Song>;
+	pub async fn read_playlist(
+		&self,
+		playlist_name: &str,
+		owner: &str,
+	) -> Result<Vec<Song>, Error> {
+		let vfs = self.vfs_manager.get_vfs().await?;
 
-		{
-			let mut connection = self.db.connect()?;
+		let songs = {
+			let mut connection = self.db.connect().await?;
 
 			// Find owner
-			let user: User = {
-				use self::users::dsl::*;
-				users
-					.filter(name.eq(owner))
-					.select((id,))
-					.first(&mut connection)
-					.optional()?
-					.ok_or(Error::UserNotFound)?
-			};
+			let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
+				.fetch_optional(connection.as_mut())
+				.await?
+				.ok_or(Error::UserNotFound)?;
 
 			// Find playlist
-			let playlist: Playlist = {
-				use self::playlists::dsl::*;
-				playlists
-					.select((id, owner))
-					.filter(name.eq(playlist_name).and(owner.eq(user.id)))
-					.get_result(&mut connection)
-					.optional()?
-					.ok_or(Error::PlaylistNotFound)?
-			};
+			let playlist_id = sqlx::query_scalar!(
+				"SELECT id FROM playlists WHERE name = $1 and owner = $2",
+				playlist_name,
+				user_id
+			)
+			.fetch_optional(connection.as_mut())
+			.await?
+			.ok_or(Error::PlaylistNotFound)?;
 
-			// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
-			let query = diesel::sql_query(
+			// List songs
+			sqlx::query_as!(
+				Song,
 				r#"
-			SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label
-			FROM playlist_songs ps
-			LEFT JOIN songs s ON ps.path = s.path
-			WHERE ps.playlist = ?
-			ORDER BY ps.ordering
-		"#,
-			);
-			let query = query.bind::<sql_types::Integer, _>(playlist.id);
-			songs = query.get_results(&mut connection)?;
-		}
+					SELECT s.*
+					FROM playlist_songs ps
+					INNER JOIN songs s ON ps.path = s.path
+					WHERE ps.playlist = $1
+					ORDER BY ps.ordering
+				"#,
+				playlist_id
+			)
+			.fetch_all(connection.as_mut())
+			.await?
+		};
 
 		// Map real path to virtual paths
 		let virtual_songs = songs
@@ -185,64 +172,30 @@ impl Manager {
 		Ok(virtual_songs)
 	}
 
-	pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
-		let mut connection = self.db.connect()?;
+	pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
+		let mut connection = self.db.connect().await?;
 
-		let user: User = {
-			use self::users::dsl::*;
-			users
-				.filter(name.eq(owner))
-				.select((id,))
-				.first(&mut connection)
-				.optional()?
-				.ok_or(Error::UserNotFound)?
-		};
+		let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
+			.fetch_optional(connection.as_mut())
+			.await?
+			.ok_or(Error::UserNotFound)?;
 
-		{
-			use self::playlists::dsl::*;
-			let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name));
-			match diesel::delete(q).execute(&mut connection)? {
-				0 => Err(Error::PlaylistNotFound),
-				_ => Ok(()),
-			}
+		let num_deletions = sqlx::query_scalar!(
+			"DELETE FROM playlists WHERE owner = $1 AND name = $2",
+			user_id,
+			playlist_name
+		)
+		.execute(connection.as_mut())
+		.await?
+		.rows_affected();
+
+		match num_deletions {
+			0 => Err(Error::PlaylistNotFound),
+			_ => Ok(()),
 		}
 	}
 }
 
-#[derive(Identifiable, Queryable, Associations)]
-#[diesel(belongs_to(User, foreign_key = owner))]
-struct Playlist {
-	id: i32,
-	owner: i32,
-}
-
-#[derive(Identifiable, Queryable, Associations)]
-#[diesel(belongs_to(Playlist, foreign_key = playlist))]
-struct PlaylistSong {
-	id: i32,
-	playlist: i32,
-}
-
-#[derive(Insertable)]
-#[diesel(table_name = playlists)]
-struct NewPlaylist {
-	name: String,
-	owner: i32,
-}
-
-#[derive(Insertable)]
-#[diesel(table_name = playlist_songs)]
-struct NewPlaylistSong {
-	playlist: i32,
-	path: String,
-	ordering: i32,
-}
-
-#[derive(Identifiable, Queryable)]
-struct User {
-	id: i32,
-}
-
 #[cfg(test)]
 mod test {
 	use std::path::{Path, PathBuf};
@@ -255,33 +208,41 @@ mod test {
 	const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
 	const TEST_MOUNT_NAME: &str = "root";
 
-	#[test]
-	fn save_playlist_golden_path() {
+	#[tokio::test]
+	async fn save_playlist_golden_path() {
 		let ctx = test::ContextBuilder::new(test_name!())
 			.user(TEST_USER, TEST_PASSWORD, false)
-			.build();
+			.build()
+			.await;
 
 		ctx.playlist_manager
 			.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
+			.await
 			.unwrap();
 
-		let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
+		let found_playlists = ctx
+			.playlist_manager
+			.list_playlists(TEST_USER)
+			.await
+			.unwrap();
 		assert_eq!(found_playlists.len(), 1);
 		assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME);
 	}
 
-	#[test]
-	fn save_playlist_is_idempotent() {
+	#[tokio::test]
+	async fn save_playlist_is_idempotent() {
 		let ctx = test::ContextBuilder::new(test_name!())
 			.user(TEST_USER, TEST_PASSWORD, false)
 			.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-			.build();
+			.build()
+			.await;
 
-		ctx.index.update().unwrap();
+		ctx.index.update().await.unwrap();
 
 		let playlist_content: Vec<String> = ctx
 			.index
 			.flatten(Path::new(TEST_MOUNT_NAME))
+			.await
 			.unwrap()
 			.into_iter()
 			.map(|s| s.path)
@@ -290,51 +251,63 @@ mod test {
 
 		ctx.playlist_manager
 			.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
+			.await
 			.unwrap();
 
 		ctx.playlist_manager
 			.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
+			.await
 			.unwrap();
 
 		let songs = ctx
 			.playlist_manager
 			.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
+			.await
 			.unwrap();
 		assert_eq!(songs.len(), 13);
 	}
 
-	#[test]
-	fn delete_playlist_golden_path() {
+	#[tokio::test]
+	async fn delete_playlist_golden_path() {
 		let ctx = test::ContextBuilder::new(test_name!())
 			.user(TEST_USER, TEST_PASSWORD, false)
-			.build();
+			.build()
+			.await;
 
 		let playlist_content = Vec::new();
 
 		ctx.playlist_manager
 			.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
+			.await
 			.unwrap();
 
 		ctx.playlist_manager
 			.delete_playlist(TEST_PLAYLIST_NAME, TEST_USER)
+			.await
 			.unwrap();
 
-		let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
+		let found_playlists = ctx
+			.playlist_manager
+			.list_playlists(TEST_USER)
+			.await
+			.unwrap();
 		assert_eq!(found_playlists.len(), 0);
 	}
 
-	#[test]
-	fn read_playlist_golden_path() {
+	#[tokio::test]
+	async fn read_playlist_golden_path() {
 		let ctx = test::ContextBuilder::new(test_name!())
 			.user(TEST_USER, TEST_PASSWORD, false)
 			.mount(TEST_MOUNT_NAME, "test-data/small-collection")
-			.build();
+			.build()
+			.await;
 
-		ctx.index.update().unwrap();
+		ctx.index.update().await.unwrap();
 
 		let playlist_content: Vec<String> = ctx
 			.index
 			.flatten(Path::new(TEST_MOUNT_NAME))
+			.await
 			.unwrap()
 			.into_iter()
 			.map(|s| s.path)
@@ -343,11 +316,13 @@ mod test {
 
 		ctx.playlist_manager
 			.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
+			.await
 			.unwrap();
 
 		let songs = ctx
 			.playlist_manager
 			.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
+			.await
 			.unwrap();
 
 		assert_eq!(songs.len(), 13);
diff --git a/src/app/settings.rs b/src/app/settings.rs
index 71ff6c7..9aca6ef 100644
--- a/src/app/settings.rs
+++ b/src/app/settings.rs
@@ -1,10 +1,8 @@
-use diesel::prelude::*;
 use regex::Regex;
 use serde::Deserialize;
-use std::convert::TryInto;
 use std::time::Duration;
 
-use crate::db::{self, misc_settings, DB};
+use crate::db::{self, DB};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -19,7 +17,7 @@ pub enum Error {
 	#[error("Index album art pattern is not a valid regex")]
 	IndexAlbumArtPatternInvalid,
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 }
 
 #[derive(Clone, Default)]
@@ -27,15 +25,15 @@ pub struct AuthSecret {
 	pub key: [u8; 32],
 }
 
-#[derive(Debug, Queryable)]
+#[derive(Debug)]
 pub struct Settings {
-	pub index_sleep_duration_seconds: i32,
+	pub index_sleep_duration_seconds: i64,
 	pub index_album_art_pattern: String,
 }
 
 #[derive(Debug, Default, Deserialize)]
 pub struct NewSettings {
-	pub reindex_every_n_seconds: Option<i32>,
+	pub reindex_every_n_seconds: Option<i64>,
 	pub album_art_pattern: Option<String>,
 }
 
@@ -49,64 +47,57 @@ impl Manager {
 		Self { db }
 	}
 
-	pub fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
-		use self::misc_settings::dsl::*;
-		let mut connection = self.db.connect()?;
-		let secret: Vec<u8> = misc_settings
-			.select(auth_secret)
-			.get_result(&mut connection)
-			.map_err(|e| match e {
-				diesel::result::Error::NotFound => Error::AuthenticationSecretNotFound,
-				e => e.into(),
-			})?;
-		secret
+	pub async fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
+		sqlx::query_scalar!("SELECT auth_secret FROM config")
+			.fetch_one(self.db.connect().await?.as_mut())
+			.await?
 			.try_into()
 			.map_err(|_| Error::AuthenticationSecretInvalid)
 			.map(|key| AuthSecret { key })
 	}
 
-	pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
-		let settings = self.read()?;
+	pub async fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
+		let settings = self.read().await?;
 		Ok(Duration::from_secs(
 			settings.index_sleep_duration_seconds as u64,
 		))
 	}
 
-	pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
-		let settings = self.read()?;
+	pub async fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
+		let settings = self.read().await?;
 		let regex = Regex::new(&format!("(?i){}", &settings.index_album_art_pattern))
 			.map_err(|_| Error::IndexAlbumArtPatternInvalid)?;
 		Ok(regex)
 	}
 
-	pub fn read(&self) -> Result<Settings, Error> {
-		use self::misc_settings::dsl::*;
-		let mut connection = self.db.connect()?;
-
-		let settings: Settings = misc_settings
-			.select((index_sleep_duration_seconds, index_album_art_pattern))
-			.get_result(&mut connection)
-			.map_err(|e| match e {
-				diesel::result::Error::NotFound => Error::MiscSettingsNotFound,
-				e => e.into(),
-			})?;
-
-		Ok(settings)
+	pub async fn read(&self) -> Result<Settings, Error> {
+		Ok(sqlx::query_as!(
+			Settings,
+			"SELECT index_sleep_duration_seconds,index_album_art_pattern FROM config"
+		)
+		.fetch_one(self.db.connect().await?.as_mut())
+		.await?)
 	}
 
-	pub fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
-		let mut connection = self.db.connect()?;
+	pub async fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
+		let mut connection = self.db.connect().await?;
 
 		if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
-			diesel::update(misc_settings::table)
-				.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration))
-				.execute(&mut connection)?;
+			sqlx::query!(
+				"UPDATE config SET index_sleep_duration_seconds = $1",
+				sleep_duration
+			)
+			.execute(connection.as_mut())
+			.await?;
 		}
 
 		if let Some(ref album_art_pattern) = new_settings.album_art_pattern {
-			diesel::update(misc_settings::table)
-				.set(misc_settings::index_album_art_pattern.eq(album_art_pattern))
-				.execute(&mut connection)?;
+			sqlx::query!(
+				"UPDATE config SET index_album_art_pattern = $1",
+				album_art_pattern
+			)
+			.execute(connection.as_mut())
+			.await?;
 		}
 
 		Ok(())
diff --git a/src/app/test.rs b/src/app/test.rs
index 7207a06..a00628c 100644
--- a/src/app/test.rs
+++ b/src/app/test.rs
@@ -1,6 +1,6 @@
 use std::path::PathBuf;
 
-use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs};
+use crate::app::{config, ddns, index::Index, playlist, settings, user, vfs};
 use crate::db::DB;
 use crate::test::*;
 
@@ -9,13 +9,10 @@ pub struct Context {
 	pub index: Index,
 	pub config_manager: config::Manager,
 	pub ddns_manager: ddns::Manager,
-	pub lastfm_manager: lastfm::Manager,
 	pub playlist_manager: playlist::Manager,
 	pub settings_manager: settings::Manager,
-	pub thumbnail_manager: thumbnail::Manager,
 	pub user_manager: user::Manager,
 	pub vfs_manager: vfs::Manager,
-	pub test_directory: PathBuf,
 }
 
 pub struct ContextBuilder {
@@ -53,14 +50,12 @@ impl ContextBuilder {
 			});
 		self
 	}
-
-	pub fn build(self) -> Context {
-		let cache_output_dir = self.test_directory.join("cache");
+	pub async fn build(self) -> Context {
 		let db_path = self.test_directory.join("db.sqlite");
 
-		let db = DB::new(&db_path).unwrap();
+		let db = DB::new(&db_path).await.unwrap();
 		let settings_manager = settings::Manager::new(db.clone());
-		let auth_secret = settings_manager.get_auth_secret().unwrap();
+		let auth_secret = settings_manager.get_auth_secret().await.unwrap();
 		let user_manager = user::Manager::new(db.clone(), auth_secret);
 		let vfs_manager = vfs::Manager::new(db.clone());
 		let ddns_manager = ddns::Manager::new(db.clone());
@@ -72,23 +67,18 @@ impl ContextBuilder {
 		);
 		let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
 		let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone());
-		let thumbnail_manager = thumbnail::Manager::new(cache_output_dir);
-		let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone());
 
-		config_manager.apply(&self.config).unwrap();
+		config_manager.apply(&self.config).await.unwrap();
 
 		Context {
 			db,
 			index,
 			config_manager,
 			ddns_manager,
-			lastfm_manager,
 			playlist_manager,
 			settings_manager,
-			thumbnail_manager,
 			user_manager,
 			vfs_manager,
-			test_directory: self.test_directory,
 		}
 	}
 }
diff --git a/src/app/user.rs b/src/app/user.rs
index b925f83..16e8234 100644
--- a/src/app/user.rs
+++ b/src/app/user.rs
@@ -1,4 +1,3 @@
-use diesel::prelude::*;
 use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
 use pbkdf2::Pbkdf2;
 use rand::rngs::OsRng;
@@ -6,12 +5,12 @@ use serde::{Deserialize, Serialize};
 use std::time::{SystemTime, UNIX_EPOCH};
 
 use crate::app::settings::AuthSecret;
-use crate::db::{self, users, DB};
+use crate::db::{self, DB};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error("Cannot use empty username")]
@@ -36,12 +35,10 @@ pub enum Error {
 	BrancaTokenEncoding,
 }
 
-#[derive(Debug, Insertable, Queryable)]
-#[diesel(table_name = users)]
+#[derive(Debug)]
 pub struct User {
 	pub name: String,
-	pub password_hash: String,
-	pub admin: i32,
+	pub admin: i64,
 }
 
 impl User {
@@ -90,61 +87,62 @@ impl Manager {
 		Self { db, auth_secret }
 	}
 
-	pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
+	pub async fn create(&self, new_user: &NewUser) -> Result<(), Error> {
 		if new_user.name.is_empty() {
 			return Err(Error::EmptyUsername);
 		}
 
 		let password_hash = hash_password(&new_user.password)?;
-		let mut connection = self.db.connect()?;
-		let new_user = User {
-			name: new_user.name.to_owned(),
+
+		sqlx::query!(
+			"INSERT INTO users (name, password_hash, admin) VALUES($1, $2, $3)",
+			new_user.name,
 			password_hash,
-			admin: new_user.admin as i32,
-		};
+			new_user.admin
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 
-		diesel::insert_into(users::table)
-			.values(&new_user)
-			.execute(&mut connection)?;
 		Ok(())
 	}
 
-	pub fn delete(&self, username: &str) -> Result<(), Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		diesel::delete(users.filter(name.eq(username))).execute(&mut connection)?;
+	pub async fn delete(&self, username: &str) -> Result<(), Error> {
+		sqlx::query!("DELETE FROM users WHERE name = $1", username)
+			.execute(self.db.connect().await?.as_mut())
+			.await?;
 		Ok(())
 	}
 
-	pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
+	pub async fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
 		let hash = hash_password(password)?;
-		let mut connection = self.db.connect()?;
-		use crate::db::users::dsl::*;
-		diesel::update(users.filter(name.eq(username)))
-			.set(password_hash.eq(hash))
-			.execute(&mut connection)?;
+		sqlx::query!(
+			"UPDATE users SET password_hash = $1 WHERE name = $2",
+			hash,
+			username
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 
-	pub fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		diesel::update(users.filter(name.eq(username)))
-			.set(admin.eq(is_admin as i32))
-			.execute(&mut connection)?;
+	pub async fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
+		sqlx::query!(
+			"UPDATE users SET admin = $1 WHERE name = $2",
+			is_admin,
+			username
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 
-	pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		match users
-			.select(password_hash)
-			.filter(name.eq(username))
-			.get_result(&mut connection)
+	pub async fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
+		match sqlx::query_scalar!("SELECT password_hash FROM users WHERE name = $1", username)
+			.fetch_optional(self.db.connect().await?.as_mut())
+			.await?
 		{
-			Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
-			Ok(hash) => {
+			None => Err(Error::IncorrectUsername),
+			Some(hash) => {
 				let hash: String = hash;
 				if verify_password(&hash, password) {
 					let authorization = Authorization {
@@ -156,17 +154,16 @@ impl Manager {
 					Err(Error::IncorrectPassword)
 				}
 			}
-			Err(e) => Err(e.into()),
 		}
 	}
 
-	pub fn authenticate(
+	pub async fn authenticate(
 		&self,
 		auth_token: &AuthToken,
 		scope: AuthorizationScope,
 	) -> Result<Authorization, Error> {
 		let authorization = self.decode_auth_token(auth_token, scope)?;
-		if self.exists(&authorization.username)? {
+		if self.exists(&authorization.username).await? {
 			Ok(authorization)
 		} else {
 			Err(Error::IncorrectUsername)
@@ -208,86 +205,76 @@ impl Manager {
 		.map(AuthToken)
 	}
 
-	pub fn count(&self) -> Result<i64, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let count = users.count().get_result(&mut connection)?;
+	pub async fn count(&self) -> Result<i32, Error> {
+		let count = sqlx::query_scalar!("SELECT COUNT(*) FROM users")
+			.fetch_one(self.db.connect().await?.as_mut())
+			.await?;
 		Ok(count)
 	}
 
-	pub fn list(&self) -> Result<Vec<User>, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let listed_users = users
-			.select((name, password_hash, admin))
-			.get_results(&mut connection)?;
+	pub async fn list(&self) -> Result<Vec<User>, Error> {
+		let listed_users = sqlx::query_as!(User, "SELECT name, admin FROM users")
+			.fetch_all(self.db.connect().await?.as_mut())
+			.await?;
 		Ok(listed_users)
 	}
 
-	pub fn exists(&self, username: &str) -> Result<bool, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let results: Vec<String> = users
-			.select(name)
-			.filter(name.eq(username))
-			.get_results(&mut connection)?;
-		Ok(!results.is_empty())
+	pub async fn exists(&self, username: &str) -> Result<bool, Error> {
+		Ok(
+			0 < sqlx::query_scalar!("SELECT COUNT(*) FROM users WHERE name = $1", username)
+				.fetch_one(self.db.connect().await?.as_mut())
+				.await?,
+		)
 	}
 
-	pub fn is_admin(&self, username: &str) -> Result<bool, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let is_admin: i32 = users
-			.filter(name.eq(username))
-			.select(admin)
-			.get_result(&mut connection)?;
-		Ok(is_admin != 0)
+	pub async fn is_admin(&self, username: &str) -> Result<bool, Error> {
+		Ok(
+			0 < sqlx::query_scalar!("SELECT admin FROM users WHERE name = $1", username)
+				.fetch_one(self.db.connect().await?.as_mut())
+				.await?,
+		)
 	}
 
-	pub fn read_preferences(&self, username: &str) -> Result<Preferences, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let (theme_base, theme_accent, read_lastfm_username) = users
-			.select((web_theme_base, web_theme_accent, lastfm_username))
-			.filter(name.eq(username))
-			.get_result(&mut connection)?;
-		Ok(Preferences {
-			web_theme_base: theme_base,
-			web_theme_accent: theme_accent,
-			lastfm_username: read_lastfm_username,
-		})
+	pub async fn read_preferences(&self, username: &str) -> Result<Preferences, Error> {
+		Ok(sqlx::query_as!(
+			Preferences,
+			"SELECT web_theme_base, web_theme_accent, lastfm_username FROM users WHERE name = $1",
+			username
+		)
+		.fetch_one(self.db.connect().await?.as_mut())
+		.await?)
 	}
 
-	pub fn write_preferences(
+	pub async fn write_preferences(
 		&self,
 		username: &str,
 		preferences: &Preferences,
 	) -> Result<(), Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		diesel::update(users.filter(name.eq(username)))
-			.set((
-				web_theme_base.eq(&preferences.web_theme_base),
-				web_theme_accent.eq(&preferences.web_theme_accent),
-			))
-			.execute(&mut connection)?;
+		sqlx::query!(
+			"UPDATE users SET web_theme_base = $1, web_theme_accent = $2 WHERE name = $3",
+			preferences.web_theme_base,
+			preferences.web_theme_accent,
+			username
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 
-	pub fn lastfm_link(
+	pub async fn lastfm_link(
 		&self,
 		username: &str,
 		lastfm_login: &str,
 		session_key: &str,
 	) -> Result<(), Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		diesel::update(users.filter(name.eq(username)))
-			.set((
-				lastfm_username.eq(lastfm_login),
-				lastfm_session_key.eq(session_key),
-			))
-			.execute(&mut connection)?;
+		sqlx::query!(
+			"UPDATE users SET lastfm_username = $1, lastfm_session_key = $2 WHERE name = $3",
+			lastfm_login,
+			session_key,
+			username
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 
@@ -298,27 +285,29 @@ impl Manager {
 		})
 	}
 
-	pub fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
-		let token: Option<String> = users
-			.filter(name.eq(username))
-			.select(lastfm_session_key)
-			.get_result(&mut connection)?;
+	pub async fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> {
+		let token: Option<String> = sqlx::query_scalar!(
+			"SELECT lastfm_session_key FROM users WHERE name = $1",
+			username
+		)
+		.fetch_one(self.db.connect().await?.as_mut())
+		.await?;
 		token.ok_or(Error::MissingLastFMSessionKey)
 	}
 
-	pub fn is_lastfm_linked(&self, username: &str) -> bool {
-		self.get_lastfm_session_key(username).is_ok()
+	pub async fn is_lastfm_linked(&self, username: &str) -> bool {
+		self.get_lastfm_session_key(username).await.is_ok()
 	}
 
-	pub fn lastfm_unlink(&self, username: &str) -> Result<(), Error> {
-		use crate::db::users::dsl::*;
-		let mut connection = self.db.connect()?;
+	pub async fn lastfm_unlink(&self, username: &str) -> Result<(), Error> {
 		let null: Option<String> = None;
-		diesel::update(users.filter(name.eq(username)))
-			.set((lastfm_session_key.eq(&null), lastfm_username.eq(&null)))
-			.execute(&mut connection)?;
+		sqlx::query!(
+			"UPDATE users SET lastfm_session_key = $1, lastfm_username = $1 WHERE name = $2",
+			null,
+			username
+		)
+		.execute(self.db.connect().await?.as_mut())
+		.await?;
 		Ok(())
 	}
 }
@@ -352,9 +341,9 @@ mod test {
 	const TEST_USERNAME: &str = "Walter";
 	const TEST_PASSWORD: &str = "super_secret!";
 
-	#[test]
-	fn create_delete_user_golden_path() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn create_delete_user_golden_path() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
@@ -362,56 +351,56 @@ mod test {
 			admin: false,
 		};
 
-		ctx.user_manager.create(&new_user).unwrap();
-		assert_eq!(ctx.user_manager.list().unwrap().len(), 1);
+		ctx.user_manager.create(&new_user).await.unwrap();
+		assert_eq!(ctx.user_manager.list().await.unwrap().len(), 1);
 
-		ctx.user_manager.delete(&new_user.name).unwrap();
-		assert_eq!(ctx.user_manager.list().unwrap().len(), 0);
+		ctx.user_manager.delete(&new_user.name).await.unwrap();
+		assert_eq!(ctx.user_manager.list().await.unwrap().len(), 0);
 	}
 
-	#[test]
-	fn cannot_create_user_with_blank_username() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn cannot_create_user_with_blank_username() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_user = NewUser {
 			name: "".to_owned(),
 			password: TEST_PASSWORD.to_owned(),
 			admin: false,
 		};
 		assert!(matches!(
-			ctx.user_manager.create(&new_user).unwrap_err(),
+			ctx.user_manager.create(&new_user).await.unwrap_err(),
 			Error::EmptyUsername
 		));
 	}
 
-	#[test]
-	fn cannot_create_user_with_blank_password() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn cannot_create_user_with_blank_password() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
 			password: "".to_owned(),
 			admin: false,
 		};
 		assert!(matches!(
-			ctx.user_manager.create(&new_user).unwrap_err(),
+			ctx.user_manager.create(&new_user).await.unwrap_err(),
 			Error::EmptyPassword
 		));
 	}
 
-	#[test]
-	fn cannot_create_duplicate_user() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn cannot_create_duplicate_user() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
 			password: TEST_PASSWORD.to_owned(),
 			admin: false,
 		};
-		ctx.user_manager.create(&new_user).unwrap();
-		ctx.user_manager.create(&new_user).unwrap_err();
+		ctx.user_manager.create(&new_user).await.unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap_err();
 	}
 
-	#[test]
-	fn can_read_write_preferences() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn can_read_write_preferences() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_preferences = Preferences {
 			web_theme_base: Some("very-dark-theme".to_owned()),
@@ -424,19 +413,20 @@ mod test {
 			password: TEST_PASSWORD.to_owned(),
 			admin: false,
 		};
-		ctx.user_manager.create(&new_user).unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap();
 
 		ctx.user_manager
 			.write_preferences(TEST_USERNAME, &new_preferences)
+			.await
 			.unwrap();
 
-		let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap();
+		let read_preferences = ctx.user_manager.read_preferences("Walter").await.unwrap();
 		assert_eq!(new_preferences, read_preferences);
 	}
 
-	#[test]
-	fn login_rejects_bad_password() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn login_rejects_bad_password() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
@@ -444,30 +434,35 @@ mod test {
 			admin: false,
 		};
 
-		ctx.user_manager.create(&new_user).unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap();
 		assert!(matches!(
 			ctx.user_manager
 				.login(TEST_USERNAME, "not the password")
+				.await
 				.unwrap_err(),
 			Error::IncorrectPassword
 		));
 	}
 
-	#[test]
-	fn login_golden_path() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn login_golden_path() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
 			password: TEST_PASSWORD.to_owned(),
 			admin: false,
 		};
-		ctx.user_manager.create(&new_user).unwrap();
-		assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok())
+		ctx.user_manager.create(&new_user).await.unwrap();
+		assert!(ctx
+			.user_manager
+			.login(TEST_USERNAME, TEST_PASSWORD)
+			.await
+			.is_ok())
 	}
 
-	#[test]
-	fn authenticate_rejects_bad_token() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn authenticate_rejects_bad_token() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
@@ -475,17 +470,18 @@ mod test {
 			admin: false,
 		};
 
-		ctx.user_manager.create(&new_user).unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap();
 		let fake_token = AuthToken("fake token".to_owned());
 		assert!(ctx
 			.user_manager
 			.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
+			.await
 			.is_err())
 	}
 
-	#[test]
-	fn authenticate_golden_path() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn authenticate_golden_path() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
@@ -493,14 +489,16 @@ mod test {
 			admin: false,
 		};
 
-		ctx.user_manager.create(&new_user).unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap();
 		let token = ctx
 			.user_manager
 			.login(TEST_USERNAME, TEST_PASSWORD)
+			.await
 			.unwrap();
 		let authorization = ctx
 			.user_manager
 			.authenticate(&token, AuthorizationScope::PolarisAuth)
+			.await
 			.unwrap();
 		assert_eq!(
 			authorization,
@@ -511,9 +509,9 @@ mod test {
 		)
 	}
 
-	#[test]
-	fn authenticate_validates_scope() {
-		let ctx = test::ContextBuilder::new(test_name!()).build();
+	#[tokio::test]
+	async fn authenticate_validates_scope() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 
 		let new_user = NewUser {
 			name: TEST_USERNAME.to_owned(),
@@ -521,14 +519,15 @@ mod test {
 			admin: false,
 		};
 
-		ctx.user_manager.create(&new_user).unwrap();
+		ctx.user_manager.create(&new_user).await.unwrap();
 		let token = ctx
 			.user_manager
 			.generate_lastfm_link_token(TEST_USERNAME)
 			.unwrap();
 		let authorization = ctx
 			.user_manager
-			.authenticate(&token, AuthorizationScope::PolarisAuth);
+			.authenticate(&token, AuthorizationScope::PolarisAuth)
+			.await;
 		assert!(matches!(
 			authorization.unwrap_err(),
 			Error::IncorrectAuthorizationScope
diff --git a/src/app/vfs.rs b/src/app/vfs.rs
index efd825b..c304515 100644
--- a/src/app/vfs.rs
+++ b/src/app/vfs.rs
@@ -1,10 +1,10 @@
 use core::ops::Deref;
-use diesel::prelude::*;
 use regex::Regex;
 use serde::{Deserialize, Serialize};
+use sqlx::{Acquire, QueryBuilder, Sqlite};
 use std::path::{self, Path, PathBuf};
 
-use crate::db::{self, mount_points, DB};
+use crate::db::{self, DB};
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -15,11 +15,10 @@ pub enum Error {
 	#[error(transparent)]
 	DatabaseConnection(#[from] db::Error),
 	#[error(transparent)]
-	Database(#[from] diesel::result::Error),
+	Database(#[from] sqlx::Error),
 }
 
-#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
-#[diesel(table_name = mount_points)]
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub struct MountDir {
 	pub source: String,
 	pub name: String,
@@ -98,31 +97,39 @@ impl Manager {
 		Self { db }
 	}
 
-	pub fn get_vfs(&self) -> Result<VFS, Error> {
-		let mount_dirs = self.mount_dirs()?;
+	pub async fn get_vfs(&self) -> Result<VFS, Error> {
+		let mount_dirs = self.mount_dirs().await?;
 		let mounts = mount_dirs.into_iter().map(|p| p.into()).collect();
 		Ok(VFS::new(mounts))
 	}
 
-	pub fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> {
-		use self::mount_points::dsl::*;
-		let mut connection = self.db.connect()?;
-		let mount_dirs: Vec<MountDir> = mount_points
-			.select((source, name))
-			.get_results(&mut connection)?;
-		Ok(mount_dirs)
+	pub async fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> {
+		Ok(
+			sqlx::query_as!(MountDir, "SELECT source, name FROM mount_points")
+				.fetch_all(self.db.connect().await?.as_mut())
+				.await?,
+		)
 	}
 
-	pub fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> {
-		let mut connection = self.db.connect()?;
-		connection.transaction::<_, diesel::result::Error, _>(|connection| {
-			use self::mount_points::dsl::*;
-			diesel::delete(mount_points).execute(&mut *connection)?;
-			diesel::insert_into(mount_points)
-				.values(mount_dirs)
-				.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
-			Ok(())
-		})?;
+	pub async fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> {
+		let mut connection = self.db.connect().await?;
+
+		connection.begin().await?;
+
+		sqlx::query!("DELETE FROM mount_points")
+			.execute(connection.as_mut())
+			.await?;
+
+		if !mount_dirs.is_empty() {
+			QueryBuilder::<Sqlite>::new("INSERT INTO mount_points(source, name) ")
+				.push_values(mount_dirs, |mut b, dir| {
+					b.push_bind(&dir.source).push_bind(&dir.name);
+				})
+				.build()
+				.execute(connection.as_mut())
+				.await?;
+		}
+
 		Ok(())
 	}
 }
diff --git a/src/db.rs b/src/db.rs
index 4f9fb36..cce477e 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -1,15 +1,13 @@
-use diesel::r2d2::{self, ConnectionManager, PooledConnection};
-use diesel::sqlite::SqliteConnection;
-use diesel::RunQueryDsl;
-use diesel_migrations::EmbeddedMigrations;
-use diesel_migrations::MigrationHarness;
 use std::path::{Path, PathBuf};
 
-mod schema;
+use sqlx::{
+	migrate::Migrator,
+	pool::PoolConnection,
+	sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
+	Sqlite,
+};
 
-pub use self::schema::*;
-
-const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
+static MIGRATOR: Migrator = sqlx::migrate!("src/db");
 
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -25,74 +23,46 @@ pub enum Error {
 
 #[derive(Clone)]
 pub struct DB {
-	pool: r2d2::Pool<ConnectionManager<SqliteConnection>>,
-}
-
-#[derive(Debug)]
-struct ConnectionCustomizer {}
-impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
-	for ConnectionCustomizer
-{
-	fn on_acquire(&self, connection: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> {
-		let query = diesel::sql_query(
-			r#"
-			PRAGMA busy_timeout = 60000;
-			PRAGMA journal_mode = WAL;
-			PRAGMA synchronous = NORMAL;
-			PRAGMA foreign_keys = ON;
-		"#,
-		);
-		query
-			.execute(connection)
-			.map_err(diesel::r2d2::Error::QueryError)?;
-		Ok(())
-	}
+	pool: SqlitePool,
 }
 
 impl DB {
-	pub fn new(path: &Path) -> Result<DB, Error> {
+	pub async fn new(path: &Path) -> Result<DB, Error> {
 		let directory = path.parent().unwrap();
 		std::fs::create_dir_all(directory).map_err(|e| Error::Io(directory.to_owned(), e))?;
-		let manager = ConnectionManager::<SqliteConnection>::new(path.to_string_lossy());
-		let pool = diesel::r2d2::Pool::builder()
-			.connection_customizer(Box::new(ConnectionCustomizer {}))
-			.build(manager)
-			.or(Err(Error::ConnectionPoolBuild))?;
+
+		let pool = SqlitePool::connect_lazy_with(
+			SqliteConnectOptions::new()
+				.create_if_missing(true)
+				.filename(path)
+				.journal_mode(SqliteJournalMode::Wal)
+				.synchronous(SqliteSynchronous::Normal),
+		);
+
 		let db = DB { pool };
-		db.migrate_up()?;
+		db.migrate_up().await?;
 		Ok(db)
 	}
 
-	pub fn connect(&self) -> Result<PooledConnection<ConnectionManager<SqliteConnection>>, Error> {
-		self.pool.get().or(Err(Error::ConnectionPool))
+	pub async fn connect(&self) -> Result<PoolConnection<Sqlite>, Error> {
+		self.pool.acquire().await.map_err(|_| Error::ConnectionPool)
 	}
 
-	#[cfg(test)]
-	fn migrate_down(&self) -> Result<(), Error> {
-		let mut connection = self.connect()?;
-		connection
-			.revert_all_migrations(MIGRATIONS)
-			.and(Ok(()))
-			.or(Err(Error::Migration))
-	}
-
-	fn migrate_up(&self) -> Result<(), Error> {
-		let mut connection = self.connect()?;
-		connection
-			.run_pending_migrations(MIGRATIONS)
+	async fn migrate_up(&self) -> Result<(), Error> {
+		MIGRATOR
+			.run(&self.pool)
+			.await
 			.and(Ok(()))
 			.or(Err(Error::Migration))
 	}
 }
 
-#[test]
-fn run_migrations() {
+#[tokio::test]
+async fn run_migrations() {
 	use crate::test::*;
 	use crate::test_name;
 	let output_dir = prepare_test_directory(test_name!());
 	let db_path = output_dir.join("db.sqlite");
-	let db = DB::new(&db_path).unwrap();
-
-	db.migrate_down().unwrap();
-	db.migrate_up().unwrap();
+	let db = DB::new(&db_path).await.unwrap();
+	db.migrate_up().await.unwrap();
 }
diff --git a/src/db/20240711080449_init.sql b/src/db/20240711080449_init.sql
new file mode 100644
index 0000000..d4b9c60
--- /dev/null
+++ b/src/db/20240711080449_init.sql
@@ -0,0 +1,95 @@
+CREATE TABLE config (
+	id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
+	auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
+	index_sleep_duration_seconds INTEGER NOT NULL,
+	index_album_art_pattern TEXT NOT NULL,
+	ddns_host TEXT NOT NULL,
+	ddns_username TEXT NOT NULL,
+	ddns_password TEXT NOT NULL
+);
+
+INSERT INTO config (
+	id,
+	auth_secret,
+	index_sleep_duration_seconds,
+	index_album_art_pattern,
+	ddns_host,
+	ddns_username,
+	ddns_password
+) VALUES (
+	0,
+	randomblob(32),
+	1800,
+	"Folder.(jpeg|jpg|png)",
+	"",
+	"",
+	""
+);
+
+CREATE TABLE mount_points (
+	id INTEGER PRIMARY KEY NOT NULL,
+	source TEXT NOT NULL,
+	name TEXT NOT NULL,
+	UNIQUE(name)
+);
+
+CREATE TABLE users (
+	id INTEGER PRIMARY KEY NOT NULL,
+	name TEXT NOT NULL,
+	password_hash TEXT NOT NULL,
+	admin INTEGER NOT NULL,
+	lastfm_username TEXT,
+	lastfm_session_key TEXT,
+	web_theme_base TEXT,
+	web_theme_accent TEXT,
+	UNIQUE(name)
+);
+
+CREATE TABLE directories (
+	id INTEGER PRIMARY KEY NOT NULL,
+	path TEXT NOT NULL,
+	parent TEXT,
+	artist TEXT,
+	year INTEGER,
+	album TEXT,
+	artwork TEXT,
+	date_added INTEGER DEFAULT 0 NOT NULL,
+	UNIQUE(path) ON CONFLICT REPLACE
+);
+
+CREATE TABLE songs (
+	id INTEGER PRIMARY KEY NOT NULL,
+	path TEXT NOT NULL,
+	parent TEXT NOT NULL,
+	track_number INTEGER,
+	disc_number INTEGER,
+	title TEXT,
+	artist TEXT,
+	album_artist TEXT,
+	year INTEGER,
+	album TEXT,
+	artwork TEXT,
+	duration INTEGER,
+	lyricist TEXT,
+	composer TEXT,
+	genre TEXT,
+	label TEXT,
+	UNIQUE(path) ON CONFLICT REPLACE
+);
+
+CREATE TABLE playlists (
+	id INTEGER PRIMARY KEY NOT NULL,
+	owner INTEGER NOT NULL,
+	name TEXT NOT NULL,
+	FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE,
+	UNIQUE(owner, name) ON CONFLICT REPLACE
+);
+
+CREATE TABLE playlist_songs (
+	id INTEGER PRIMARY KEY NOT NULL,
+	playlist INTEGER NOT NULL,
+	path TEXT NOT NULL,
+	ordering INTEGER NOT NULL,
+	FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE(playlist, ordering) ON CONFLICT REPLACE
+);
diff --git a/src/db/schema.rs b/src/db/schema.rs
deleted file mode 100644
index dd1b512..0000000
--- a/src/db/schema.rs
+++ /dev/null
@@ -1,103 +0,0 @@
-table! {
-	ddns_config (id) {
-		id -> Integer,
-		host -> Text,
-		username -> Text,
-		password -> Text,
-	}
-}
-
-table! {
-	directories (id) {
-		id -> Integer,
-		path -> Text,
-		parent -> Nullable<Text>,
-		artist -> Nullable<Text>,
-		year -> Nullable<Integer>,
-		album -> Nullable<Text>,
-		artwork -> Nullable<Text>,
-		date_added -> Integer,
-	}
-}
-
-table! {
-	misc_settings (id) {
-		id -> Integer,
-		auth_secret -> Binary,
-		index_sleep_duration_seconds -> Integer,
-		index_album_art_pattern -> Text,
-	}
-}
-
-table! {
-	mount_points (id) {
-		id -> Integer,
-		source -> Text,
-		name -> Text,
-	}
-}
-
-table! {
-	playlist_songs (id) {
-		id -> Integer,
-		playlist -> Integer,
-		path -> Text,
-		ordering -> Integer,
-	}
-}
-
-table! {
-	playlists (id) {
-		id -> Integer,
-		owner -> Integer,
-		name -> Text,
-	}
-}
-
-table! {
-	songs (id) {
-		id -> Integer,
-		path -> Text,
-		parent -> Text,
-		track_number -> Nullable<Integer>,
-		disc_number -> Nullable<Integer>,
-		title -> Nullable<Text>,
-		artist -> Nullable<Text>,
-		album_artist -> Nullable<Text>,
-		year -> Nullable<Integer>,
-		album -> Nullable<Text>,
-		artwork -> Nullable<Text>,
-		duration -> Nullable<Integer>,
-		lyricist -> Nullable<Text>,
-		composer -> Nullable<Text>,
-		genre -> Nullable<Text>,
-		label -> Nullable<Text>,
-	}
-}
-
-table! {
-	users (id) {
-		id -> Integer,
-		name -> Text,
-		password_hash -> Text,
-		admin -> Integer,
-		lastfm_username -> Nullable<Text>,
-		lastfm_session_key -> Nullable<Text>,
-		web_theme_base -> Nullable<Text>,
-		web_theme_accent -> Nullable<Text>,
-	}
-}
-
-joinable!(playlist_songs -> playlists (playlist));
-joinable!(playlists -> users (owner));
-
-allow_tables_to_appear_in_same_query!(
-	ddns_config,
-	directories,
-	misc_settings,
-	mount_points,
-	playlist_songs,
-	playlists,
-	songs,
-	users,
-);
diff --git a/src/main.rs b/src/main.rs
index df07abc..a265812 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,12 +1,8 @@
 #![cfg_attr(all(windows, feature = "ui"), windows_subsystem = "windows")]
 #![recursion_limit = "256"]
 
-#[macro_use]
-extern crate diesel;
-#[macro_use]
-extern crate diesel_migrations;
-
-use log::info;
+use log::{error, info};
+use options::CLIOptions;
 use simplelog::{
 	ColorChoice, CombinedLogger, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger,
 };
@@ -27,6 +23,8 @@ mod utils;
 pub enum Error {
 	#[error(transparent)]
 	App(#[from] app::Error),
+	#[error("Could not start web services")]
+	ServiceStartup(std::io::Error),
 	#[error("Could not parse command line arguments:\n\n{0}")]
 	CliArgsParsing(getopts::Fail),
 	#[cfg(unix)]
@@ -139,16 +137,21 @@ fn main() -> Result<(), Error> {
 	info!("Swagger files location is {:#?}", paths.swagger_dir_path);
 	info!("Web client files location is {:#?}", paths.web_dir_path);
 
+	async_main(cli_options, paths)
+}
+
+#[tokio::main]
+async fn async_main(cli_options: CLIOptions, paths: paths::Paths) -> Result<(), Error> {
 	// Create and run app
-	let app = app::App::new(cli_options.port.unwrap_or(5050), paths)?;
+	let app = app::App::new(cli_options.port.unwrap_or(5050), paths).await?;
 	app.index.begin_periodic_updates();
 	app.ddns_manager.begin_periodic_updates();
 
 	// Start server
 	info!("Starting up server");
-	std::thread::spawn(move || {
-		let _ = service::run(app);
-	});
+	if let Err(e) = service::launch(app) {
+		return Err(Error::ServiceStartup(e));
+	}
 
 	// Send readiness notification
 	#[cfg(unix)]
diff --git a/src/paths.rs b/src/paths.rs
index 207eab5..601681b 100644
--- a/src/paths.rs
+++ b/src/paths.rs
@@ -80,23 +80,23 @@ impl Paths {
 	pub fn new(cli_options: &CLIOptions) -> Self {
 		let mut paths = Self::from_build();
 		if let Some(path) = &cli_options.cache_dir_path {
-			paths.cache_dir_path = path.clone();
+			path.clone_into(&mut paths.cache_dir_path);
 		}
 		if let Some(path) = &cli_options.config_file_path {
 			paths.config_file_path = Some(path.clone());
 		}
 		if let Some(path) = &cli_options.database_file_path {
-			paths.db_file_path = path.clone();
+			path.clone_into(&mut paths.db_file_path);
 		}
 		#[cfg(unix)]
 		if let Some(path) = &cli_options.pid_file_path {
-			paths.pid_file_path = path.clone();
+			path.clone_into(&mut paths.pid_file_path);
 		}
 		if let Some(path) = &cli_options.swagger_dir_path {
-			paths.swagger_dir_path = path.clone();
+			path.clone_into(&mut paths.swagger_dir_path);
 		}
 		if let Some(path) = &cli_options.web_dir_path {
-			paths.web_dir_path = path.clone();
+			path.clone_into(&mut paths.web_dir_path);
 		}
 
 		let log_to_file = cli_options.log_file_path.is_some() || !cli_options.foreground;
diff --git a/src/service/actix.rs b/src/service/actix.rs
index 0fce678..56de21d 100644
--- a/src/service/actix.rs
+++ b/src/service/actix.rs
@@ -1,7 +1,6 @@
 use actix_web::{
 	dev::Service,
 	middleware::{Compress, Logger, NormalizePath},
-	rt::System,
 	web::{self, ServiceConfig},
 	App as ActixApp, HttpServer,
 };
@@ -43,9 +42,9 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
 	}
 }
 
-pub fn run(app: App) -> Result<(), std::io::Error> {
+pub fn launch(app: App) -> Result<(), std::io::Error> {
 	let address = ("0.0.0.0", app.port);
-	System::new().block_on(
+	tokio::spawn(
 		HttpServer::new(move || {
 			ActixApp::new()
 				.wrap(Logger::default())
@@ -72,5 +71,6 @@ pub fn run(app: App) -> Result<(), std::io::Error> {
 			e
 		})?
 		.run(),
-	)
+	);
+	Ok(())
 }
diff --git a/src/service/actix/api.rs b/src/service/actix/api.rs
index d3600ee..a84fd50 100644
--- a/src/service/actix/api.rs
+++ b/src/service/actix/api.rs
@@ -16,7 +16,7 @@ use base64::prelude::*;
 use futures_util::future::err;
 use percent_encoding::percent_decode_str;
 use std::future::Future;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::pin::Pin;
 use std::str;
 
@@ -141,25 +141,27 @@ impl FromRequest for Auth {
 			// Auth via bearer token in query parameter
 			if let Ok(query) = query_params_future.await {
 				let auth_token = user::AuthToken(query.auth_token.clone());
-				let authorization = block(move || {
-					user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
-				})
-				.await?;
-				return Ok(Auth {
-					username: authorization.username,
-				});
+				if let Ok(auth) = user_manager
+					.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
+					.await
+				{
+					return Ok(Auth {
+						username: auth.username,
+					});
+				}
 			}
 
 			// Auth via bearer token in authorization header
 			if let Ok(bearer_auth) = bearer_auth_future.await {
 				let auth_token = user::AuthToken(bearer_auth.token().to_owned());
-				let authorization = block(move || {
-					user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
-				})
-				.await?;
-				return Ok(Auth {
-					username: authorization.username,
-				});
+				if let Ok(auth) = user_manager
+					.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
+					.await
+				{
+					return Ok(Auth {
+						username: auth.username,
+					});
+				}
 			}
 
 			Err(ErrorUnauthorized(APIError::AuthenticationRequired))
@@ -185,21 +187,18 @@ impl FromRequest for AdminRights {
 		let auth_future = Auth::from_request(request, payload);
 
 		Box::pin(async move {
-			let user_manager_count = user_manager.clone();
-			let user_count = block(move || user_manager_count.count()).await;
+			let user_count = user_manager.count().await;
 			match user_count {
-				Err(e) => return Err(e.into()),
+				Err(_) => return Err(ErrorInternalServerError(APIError::Internal)),
 				Ok(0) => return Ok(AdminRights { auth: None }),
 				_ => (),
 			};
 
 			let auth = auth_future.await?;
-			let username = auth.username.clone();
-			let is_admin = block(move || user_manager.is_admin(&username)).await?;
-			if is_admin {
-				Ok(AdminRights { auth: Some(auth) })
-			} else {
-				Err(ErrorForbidden(APIError::AdminPermissionRequired))
+			match user_manager.is_admin(&auth.username).await {
+				Ok(true) => Ok(AdminRights { auth: Some(auth) }),
+				Ok(false) => Err(ErrorForbidden(APIError::AdminPermissionRequired)),
+				Err(_) => Err(ErrorInternalServerError(APIError::Internal)),
 			}
 		})
 	}
@@ -228,18 +227,6 @@ impl Responder for MediaFile {
 	}
 }
 
-async fn block<F, I, E>(f: F) -> Result<I, APIError>
-where
-	F: FnOnce() -> Result<I, E> + Send + 'static,
-	I: Send + 'static,
-	E: Send + std::fmt::Debug + 'static + Into<APIError>,
-{
-	actix_web::web::block(f)
-		.await
-		.map_err(|_| APIError::Internal)
-		.and_then(|r| r.map_err(|e| e.into()))
-}
-
 #[get("/version")]
 async fn version() -> Json<dto::Version> {
 	let current_version = dto::Version {
@@ -253,14 +240,13 @@ async fn version() -> Json<dto::Version> {
 async fn initial_setup(
 	user_manager: Data<user::Manager>,
 ) -> Result<Json<dto::InitialSetup>, APIError> {
-	let initial_setup = block(move || -> Result<dto::InitialSetup, APIError> {
-		let users = user_manager.list()?;
+	let initial_setup = {
+		let users = user_manager.list().await?;
 		let has_any_admin = users.iter().any(|u| u.is_admin());
-		Ok(dto::InitialSetup {
+		dto::InitialSetup {
 			has_any_users: has_any_admin,
-		})
-	})
-	.await?;
+		}
+	};
 	Ok(Json(initial_setup))
 }
 
@@ -270,7 +256,7 @@ async fn apply_config(
 	config_manager: Data<config::Manager>,
 	config: Json<dto::Config>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || config_manager.apply(&config.to_owned().into())).await?;
+	config_manager.apply(&config.to_owned().into()).await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -279,7 +265,7 @@ async fn get_settings(
 	settings_manager: Data<settings::Manager>,
 	_admin_rights: AdminRights,
 ) -> Result<Json<dto::Settings>, APIError> {
-	let settings = block(move || settings_manager.read()).await?;
+	let settings = settings_manager.read().await?;
 	Ok(Json(settings.into()))
 }
 
@@ -289,7 +275,9 @@ async fn put_settings(
 	settings_manager: Data<settings::Manager>,
 	new_settings: Json<dto::NewSettings>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || settings_manager.amend(&new_settings.to_owned().into())).await?;
+	settings_manager
+		.amend(&new_settings.to_owned().into())
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -298,7 +286,7 @@ async fn list_mount_dirs(
 	vfs_manager: Data<vfs::Manager>,
 	_admin_rights: AdminRights,
 ) -> Result<Json<Vec<dto::MountDir>>, APIError> {
-	let mount_dirs = block(move || vfs_manager.mount_dirs()).await?;
+	let mount_dirs = vfs_manager.mount_dirs().await?;
 	let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect();
 	Ok(Json(mount_dirs))
 }
@@ -310,7 +298,7 @@ async fn put_mount_dirs(
 	new_mount_dirs: Json<Vec<dto::MountDir>>,
 ) -> Result<HttpResponse, APIError> {
 	let new_mount_dirs: Vec<MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect();
-	block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?;
+	vfs_manager.set_mount_dirs(&new_mount_dirs).await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -319,7 +307,7 @@ async fn get_ddns_config(
 	ddns_manager: Data<ddns::Manager>,
 	_admin_rights: AdminRights,
 ) -> Result<Json<dto::DDNSConfig>, APIError> {
-	let ddns_config = block(move || ddns_manager.config()).await?;
+	let ddns_config = ddns_manager.config().await?;
 	Ok(Json(ddns_config.into()))
 }
 
@@ -329,7 +317,9 @@ async fn put_ddns_config(
 	ddns_manager: Data<ddns::Manager>,
 	new_ddns_config: Json<dto::DDNSConfig>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || ddns_manager.set_config(&new_ddns_config.to_owned().into())).await?;
+	ddns_manager
+		.set_config(&new_ddns_config.to_owned().into())
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -338,7 +328,7 @@ async fn list_users(
 	user_manager: Data<user::Manager>,
 	_admin_rights: AdminRights,
 ) -> Result<Json<Vec<dto::User>>, APIError> {
-	let users = block(move || user_manager.list()).await?;
+	let users = user_manager.list().await?;
 	let users = users.into_iter().map(|u| u.into()).collect();
 	Ok(Json(users))
 }
@@ -350,7 +340,7 @@ async fn create_user(
 	new_user: Json<dto::NewUser>,
 ) -> Result<HttpResponse, APIError> {
 	let new_user = new_user.to_owned().into();
-	block(move || user_manager.create(&new_user)).await?;
+	user_manager.create(&new_user).await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -367,16 +357,14 @@ async fn update_user(
 		}
 	}
 
-	block(move || -> Result<(), APIError> {
-		if let Some(password) = &user_update.new_password {
-			user_manager.set_password(&name, password)?;
-		}
-		if let Some(is_admin) = &user_update.new_is_admin {
-			user_manager.set_is_admin(&name, *is_admin)?;
-		}
-		Ok(())
-	})
-	.await?;
+	if let Some(password) = &user_update.new_password {
+		user_manager.set_password(&name, password).await?;
+	}
+
+	if let Some(is_admin) = &user_update.new_is_admin {
+		user_manager.set_is_admin(&name, *is_admin).await?;
+	}
+
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -391,7 +379,7 @@ async fn delete_user(
 			return Err(APIError::DeletingOwnAccount);
 		}
 	}
-	block(move || user_manager.delete(&name)).await?;
+	user_manager.delete(&name).await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -400,7 +388,7 @@ async fn get_preferences(
 	user_manager: Data<user::Manager>,
 	auth: Auth,
 ) -> Result<Json<user::Preferences>, APIError> {
-	let preferences = block(move || user_manager.read_preferences(&auth.username)).await?;
+	let preferences = user_manager.read_preferences(&auth.username).await?;
 	Ok(Json(preferences))
 }
 
@@ -410,7 +398,9 @@ async fn put_preferences(
 	auth: Auth,
 	preferences: Json<user::Preferences>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || user_manager.write_preferences(&auth.username, &preferences)).await?;
+	user_manager
+		.write_preferences(&auth.username, &preferences)
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -429,18 +419,18 @@ async fn login(
 	credentials: Json<dto::Credentials>,
 ) -> Result<HttpResponse, APIError> {
 	let username = credentials.username.clone();
-	let (user::AuthToken(token), is_admin) =
-		block(move || -> Result<(user::AuthToken, bool), APIError> {
-			let auth_token = user_manager.login(&credentials.username, &credentials.password)?;
-			let is_admin = user_manager.is_admin(&credentials.username)?;
-			Ok((auth_token, is_admin))
-		})
+
+	let user::AuthToken(token) = user_manager
+		.login(&credentials.username, &credentials.password)
 		.await?;
+	let is_admin = user_manager.is_admin(&credentials.username).await?;
+
 	let authorization = dto::Authorization {
 		username: username.clone(),
 		token,
 		is_admin,
 	};
+
 	let response = HttpResponse::Ok().json(authorization);
 	Ok(response)
 }
@@ -450,7 +440,7 @@ async fn browse_root(
 	index: Data<Index>,
 	_auth: Auth,
 ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
-	let result = block(move || index.browse(Path::new(""))).await?;
+	let result = index.browse(Path::new("")).await?;
 	Ok(Json(result))
 }
 
@@ -460,17 +450,14 @@ async fn browse(
 	_auth: Auth,
 	path: web::Path<String>,
 ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
-	let result = block(move || {
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		index.browse(Path::new(path.as_ref()))
-	})
-	.await?;
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	let result = index.browse(Path::new(path.as_ref())).await?;
 	Ok(Json(result))
 }
 
 #[get("/flatten")]
 async fn flatten_root(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Song>>, APIError> {
-	let songs = block(move || index.flatten(Path::new(""))).await?;
+	let songs = index.flatten(Path::new("")).await?;
 	Ok(Json(songs))
 }
 
@@ -480,23 +467,20 @@ async fn flatten(
 	_auth: Auth,
 	path: web::Path<String>,
 ) -> Result<Json<Vec<index::Song>>, APIError> {
-	let songs = block(move || {
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		index.flatten(Path::new(path.as_ref()))
-	})
-	.await?;
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	let songs = index.flatten(Path::new(path.as_ref())).await?;
 	Ok(Json(songs))
 }
 
 #[get("/random")]
 async fn random(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> {
-	let result = block(move || index.get_random_albums(20)).await?;
+	let result = index.get_random_albums(20).await?;
 	Ok(Json(result))
 }
 
 #[get("/recent")]
 async fn recent(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> {
-	let result = block(move || index.get_recent_albums(20)).await?;
+	let result = index.get_recent_albums(20).await?;
 	Ok(Json(result))
 }
 
@@ -505,7 +489,7 @@ async fn search_root(
 	index: Data<Index>,
 	_auth: Auth,
 ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
-	let result = block(move || index.search("")).await?;
+	let result = index.search("").await?;
 	Ok(Json(result))
 }
 
@@ -515,7 +499,7 @@ async fn search(
 	_auth: Auth,
 	query: web::Path<String>,
 ) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
-	let result = block(move || index.search(&query)).await?;
+	let result = index.search(&query).await?;
 	Ok(Json(result))
 }
 
@@ -525,13 +509,9 @@ async fn get_audio(
 	_auth: Auth,
 	path: web::Path<String>,
 ) -> Result<MediaFile, APIError> {
-	let audio_path = block(move || {
-		let vfs = vfs_manager.get_vfs()?;
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		vfs.virtual_to_real(Path::new(path.as_ref()))
-	})
-	.await?;
-
+	let vfs = vfs_manager.get_vfs().await?;
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	let audio_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
 	let named_file = NamedFile::open(audio_path).map_err(|_| APIError::AudioFileIOError)?;
 	Ok(MediaFile::new(named_file))
 }
@@ -545,19 +525,11 @@ async fn get_thumbnail(
 	options_input: web::Query<dto::ThumbnailOptions>,
 ) -> Result<MediaFile, APIError> {
 	let options = thumbnail::Options::from(options_input.0);
-
-	let thumbnail_path = block(move || -> Result<PathBuf, APIError> {
-		let vfs = vfs_manager.get_vfs()?;
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
-		thumbnails_manager
-			.get_thumbnail(&image_path, &options)
-			.map_err(|e| e.into())
-	})
-	.await?;
-
+	let vfs = vfs_manager.get_vfs().await?;
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
+	let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?;
 	let named_file = NamedFile::open(thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?;
-
 	Ok(MediaFile::new(named_file))
 }
 
@@ -566,7 +538,7 @@ async fn list_playlists(
 	playlist_manager: Data<playlist::Manager>,
 	auth: Auth,
 ) -> Result<Json<Vec<dto::ListPlaylistsEntry>>, APIError> {
-	let playlist_names = block(move || playlist_manager.list_playlists(&auth.username)).await?;
+	let playlist_names = playlist_manager.list_playlists(&auth.username).await?;
 	let playlists: Vec<dto::ListPlaylistsEntry> = playlist_names
 		.into_iter()
 		.map(|p| dto::ListPlaylistsEntry { name: p })
@@ -582,7 +554,9 @@ async fn save_playlist(
 	name: web::Path<String>,
 	playlist: Json<dto::SavePlaylistInput>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || playlist_manager.save_playlist(&name, &auth.username, &playlist.tracks)).await?;
+	playlist_manager
+		.save_playlist(&name, &auth.username, &playlist.tracks)
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -592,7 +566,9 @@ async fn read_playlist(
 	auth: Auth,
 	name: web::Path<String>,
 ) -> Result<Json<Vec<index::Song>>, APIError> {
-	let songs = block(move || playlist_manager.read_playlist(&name, &auth.username)).await?;
+	let songs = playlist_manager
+		.read_playlist(&name, &auth.username)
+		.await?;
 	Ok(Json(songs))
 }
 
@@ -602,7 +578,9 @@ async fn delete_playlist(
 	auth: Auth,
 	name: web::Path<String>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || playlist_manager.delete_playlist(&name, &auth.username)).await?;
+	playlist_manager
+		.delete_playlist(&name, &auth.username)
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -613,15 +591,13 @@ async fn lastfm_now_playing(
 	auth: Auth,
 	path: web::Path<String>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || -> Result<(), APIError> {
-		if !user_manager.is_lastfm_linked(&auth.username) {
-			return Err(APIError::LastFMAccountNotLinked);
-		}
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?;
-		Ok(())
-	})
-	.await?;
+	if !user_manager.is_lastfm_linked(&auth.username).await {
+		return Err(APIError::LastFMAccountNotLinked);
+	}
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	lastfm_manager
+		.now_playing(&auth.username, Path::new(path.as_ref()))
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -632,15 +608,13 @@ async fn lastfm_scrobble(
 	auth: Auth,
 	path: web::Path<String>,
 ) -> Result<HttpResponse, APIError> {
-	block(move || -> Result<(), APIError> {
-		if !user_manager.is_lastfm_linked(&auth.username) {
-			return Err(APIError::LastFMAccountNotLinked);
-		}
-		let path = percent_decode_str(&path).decode_utf8_lossy();
-		lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?;
-		Ok(())
-	})
-	.await?;
+	if !user_manager.is_lastfm_linked(&auth.username).await {
+		return Err(APIError::LastFMAccountNotLinked);
+	}
+	let path = percent_decode_str(&path).decode_utf8_lossy();
+	lastfm_manager
+		.scrobble(&auth.username, Path::new(path.as_ref()))
+		.await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
 
@@ -649,8 +623,7 @@ async fn lastfm_link_token(
 	lastfm_manager: Data<lastfm::Manager>,
 	auth: Auth,
 ) -> Result<Json<dto::LastFMLinkToken>, APIError> {
-	let user::AuthToken(value) =
-		block(move || lastfm_manager.generate_link_token(&auth.username)).await?;
+	let user::AuthToken(value) = lastfm_manager.generate_link_token(&auth.username)?;
 	Ok(Json(dto::LastFMLinkToken { value }))
 }
 
@@ -660,27 +633,27 @@ async fn lastfm_link(
 	user_manager: Data<user::Manager>,
 	payload: web::Query<dto::LastFMLink>,
 ) -> Result<HttpResponse, APIError> {
-	let popup_content_string = block(move || {
-		let auth_token = user::AuthToken(payload.auth_token.clone());
-		let authorization =
-			user_manager.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)?;
-		let lastfm_token = &payload.token;
-		lastfm_manager.link(&authorization.username, lastfm_token)?;
+	let auth_token = user::AuthToken(payload.auth_token.clone());
+	let authorization = user_manager
+		.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)
+		.await?;
+	let lastfm_token = &payload.token;
+	lastfm_manager
+		.link(&authorization.username, lastfm_token)
+		.await?;
 
-		// Percent decode
-		let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
+	// Percent decode
+	let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
 
-		// Base64 decode
-		let popup_content = BASE64_STANDARD_NO_PAD
-			.decode(base64_content.as_bytes())
-			.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
+	// Base64 decode
+	let popup_content = BASE64_STANDARD_NO_PAD
+		.decode(base64_content.as_bytes())
+		.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
 
-		// UTF-8 decode
-		str::from_utf8(&popup_content)
-			.map_err(|_| APIError::LastFMLinkContentEncodingError)
-			.map(|s| s.to_owned())
-	})
-	.await?;
+	// UTF-8 decode
+	let popup_content_string = str::from_utf8(&popup_content)
+		.map_err(|_| APIError::LastFMLinkContentEncodingError)
+		.map(|s| s.to_owned())?;
 
 	Ok(HttpResponse::build(StatusCode::OK)
 		.content_type("text/html; charset=utf-8")
@@ -692,6 +665,6 @@ async fn lastfm_unlink(
 	lastfm_manager: Data<lastfm::Manager>,
 	auth: Auth,
 ) -> Result<HttpResponse, APIError> {
-	block(move || lastfm_manager.unlink(&auth.username)).await?;
+	lastfm_manager.unlink(&auth.username).await?;
 	Ok(HttpResponse::new(StatusCode::OK))
 }
diff --git a/src/service/actix/test.rs b/src/service/actix/test.rs
index 3a2c94d..7312f56 100644
--- a/src/service/actix/test.rs
+++ b/src/service/actix/test.rs
@@ -1,7 +1,6 @@
 use actix_test::TestServer;
 use actix_web::{
 	middleware::{Compress, Logger},
-	rt::{System, SystemRunner},
 	web::Bytes,
 	App as ActixApp,
 };
@@ -18,7 +17,6 @@ use crate::service::test::TestService;
 use crate::test::*;
 
 pub struct ActixTestService {
-	system_runner: SystemRunner,
 	authorization: Option<dto::Authorization>,
 	server: TestServer,
 }
@@ -26,7 +24,7 @@ pub struct ActixTestService {
 pub type ServiceType = ActixTestService;
 
 impl ActixTestService {
-	fn process_internal<T: Serialize + Clone + 'static>(
+	async fn process_internal<T: Serialize + Clone + 'static>(
 		&mut self,
 		request: &Request<T>,
 	) -> (Builder, Option<Bytes>) {
@@ -50,9 +48,7 @@ impl ActixTestService {
 			actix_request = actix_request.bearer_auth(&authorization.token);
 		}
 
-		let mut actix_response = self
-			.system_runner
-			.block_on(async move { actix_request.send_json(&body).await.unwrap() });
+		let mut actix_response = actix_request.send_json(&body).await.unwrap();
 
 		let mut response_builder = Response::builder().status(actix_response.status());
 		let headers = response_builder.headers_mut().unwrap();
@@ -62,10 +58,7 @@ impl ActixTestService {
 
 		let is_success = actix_response.status().is_success();
 		let body = if is_success {
-			Some(
-				self.system_runner
-					.block_on(async move { actix_response.body().await.unwrap() }),
-			)
+			Some(actix_response.body().await.unwrap())
 		} else {
 			None
 		};
@@ -75,7 +68,7 @@ impl ActixTestService {
 }
 
 impl TestService for ActixTestService {
-	fn new(test_name: &str) -> Self {
+	async fn new(test_name: &str) -> Self {
 		let output_dir = prepare_test_directory(test_name);
 
 		let paths = Paths {
@@ -89,9 +82,8 @@ impl TestService for ActixTestService {
 			web_dir_path: ["test-data", "web"].iter().collect(),
 		};
 
-		let app = App::new(5050, paths).unwrap();
+		let app = App::new(5050, paths).await.unwrap();
 
-		let system_runner = System::new();
 		let server = actix_test::start(move || {
 			let config = make_config(app.clone());
 			ActixApp::new()
@@ -102,31 +94,33 @@ impl TestService for ActixTestService {
 
 		ActixTestService {
 			authorization: None,
-			system_runner,
 			server,
 		}
 	}
 
-	fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()> {
-		let (response_builder, _body) = self.process_internal(request);
+	async fn fetch<T: Serialize + Clone + 'static>(
+		&mut self,
+		request: &Request<T>,
+	) -> Response<()> {
+		let (response_builder, _body) = self.process_internal(request).await;
 		response_builder.body(()).unwrap()
 	}
 
-	fn fetch_bytes<T: Serialize + Clone + 'static>(
+	async fn fetch_bytes<T: Serialize + Clone + 'static>(
 		&mut self,
 		request: &Request<T>,
 	) -> Response<Vec<u8>> {
-		let (response_builder, body) = self.process_internal(request);
+		let (response_builder, body) = self.process_internal(request).await;
 		response_builder
 			.body(body.unwrap().deref().to_owned())
 			.unwrap()
 	}
 
-	fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
+	async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
 		&mut self,
 		request: &Request<T>,
 	) -> Response<U> {
-		let (response_builder, body) = self.process_internal(request);
+		let (response_builder, body) = self.process_internal(request).await;
 		let body = serde_json::from_slice(&body.unwrap()).unwrap();
 		response_builder.body(body).unwrap()
 	}
diff --git a/src/service/dto.rs b/src/service/dto.rs
index 8bd79a3..161fb3b 100644
--- a/src/service/dto.rs
+++ b/src/service/dto.rs
@@ -139,9 +139,9 @@ pub struct DDNSConfig {
 impl From<DDNSConfig> for ddns::Config {
 	fn from(c: DDNSConfig) -> Self {
 		Self {
-			host: c.host,
-			username: c.username,
-			password: c.password,
+			ddns_host: c.host,
+			ddns_username: c.username,
+			ddns_password: c.password,
 		}
 	}
 }
@@ -149,9 +149,9 @@ impl From<DDNSConfig> for ddns::Config {
 impl From<ddns::Config> for DDNSConfig {
 	fn from(c: ddns::Config) -> Self {
 		Self {
-			host: c.host,
-			username: c.username,
-			password: c.password,
+			host: c.ddns_host,
+			username: c.ddns_username,
+			password: c.ddns_password,
 		}
 	}
 }
@@ -204,7 +204,7 @@ impl From<Config> for config::Config {
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct NewSettings {
 	pub album_art_pattern: Option<String>,
-	pub reindex_every_n_seconds: Option<i32>,
+	pub reindex_every_n_seconds: Option<i64>,
 }
 
 impl From<NewSettings> for settings::NewSettings {
@@ -219,7 +219,7 @@ impl From<NewSettings> for settings::NewSettings {
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Settings {
 	pub album_art_pattern: String,
-	pub reindex_every_n_seconds: i32,
+	pub reindex_every_n_seconds: i64,
 }
 
 impl From<settings::Settings> for Settings {
diff --git a/src/service/error.rs b/src/service/error.rs
index 0f6ee4e..18bd337 100644
--- a/src/service/error.rs
+++ b/src/service/error.rs
@@ -18,7 +18,7 @@ pub enum APIError {
 	#[error("Could not encode Branca token")]
 	BrancaTokenEncoding,
 	#[error("Database error:\n\n{0}")]
-	Database(diesel::result::Error),
+	Database(sqlx::Error),
 	#[error("DDNS update query failed with HTTP status {0}")]
 	DdnsUpdateQueryFailed(u16),
 	#[error("Cannot delete your own account")]
diff --git a/src/service/test.rs b/src/service/test.rs
index 0544306..6a35276 100644
--- a/src/service/test.rs
+++ b/src/service/test.rs
@@ -26,18 +26,19 @@ use crate::service::test::constants::*;
 pub use crate::service::actix::test::ServiceType;
 
 pub trait TestService {
-	fn new(test_name: &str) -> Self;
-	fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()>;
-	fn fetch_bytes<T: Serialize + Clone + 'static>(
+	async fn new(test_name: &str) -> Self;
+	async fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>)
+		-> Response<()>;
+	async fn fetch_bytes<T: Serialize + Clone + 'static>(
 		&mut self,
 		request: &Request<T>,
 	) -> Response<Vec<u8>>;
-	fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
+	async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
 		&mut self,
 		request: &Request<T>,
 	) -> Response<U>;
 
-	fn complete_initial_setup(&mut self) {
+	async fn complete_initial_setup(&mut self) {
 		let configuration = dto::Config {
 			users: Some(vec![
 				dto::NewUser {
@@ -58,40 +59,43 @@ pub trait TestService {
 			..Default::default()
 		};
 		let request = protocol::apply_config(configuration);
-		let response = self.fetch(&request);
+		let response = self.fetch(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 	}
 
-	fn login_internal(&mut self, username: &str, password: &str) {
+	async fn login_internal(&mut self, username: &str, password: &str) {
 		let request = protocol::login(username, password);
-		let response = self.fetch_json::<_, dto::Authorization>(&request);
+		let response = self.fetch_json::<_, dto::Authorization>(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 		let authorization = response.into_body();
 		self.set_authorization(Some(authorization));
 	}
 
-	fn login_admin(&mut self) {
-		self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
+	async fn login_admin(&mut self) {
+		self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN)
+			.await;
 	}
 
-	fn login(&mut self) {
-		self.login_internal(TEST_USERNAME, TEST_PASSWORD);
+	async fn login(&mut self) {
+		self.login_internal(TEST_USERNAME, TEST_PASSWORD).await;
 	}
 
-	fn logout(&mut self) {
+	async fn logout(&mut self) {
 		self.set_authorization(None);
 	}
 
 	fn set_authorization(&mut self, authorization: Option<dto::Authorization>);
 
-	fn index(&mut self) {
+	async fn index(&mut self) {
 		let request = protocol::trigger_index();
-		let response = self.fetch(&request);
+		let response = self.fetch(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 
 		loop {
 			let browse_request = protocol::browse(Path::new(""));
-			let response = self.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request);
+			let response = self
+				.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request)
+				.await;
 			let entries = response.body();
 			if !entries.is_empty() {
 				break;
@@ -101,7 +105,9 @@ pub trait TestService {
 
 		loop {
 			let flatten_request = protocol::flatten(Path::new(""));
-			let response = self.fetch_json::<_, Vec<index::Song>>(&flatten_request);
+			let response = self
+				.fetch_json::<_, Vec<index::Song>>(&flatten_request)
+				.await;
 			let entries = response.body();
 			if !entries.is_empty() {
 				break;
diff --git a/src/service/test/admin.rs b/src/service/test/admin.rs
index f5e679f..64e1f3b 100644
--- a/src/service/test/admin.rs
+++ b/src/service/test/admin.rs
@@ -5,20 +5,20 @@ use crate::service::dto;
 use crate::service::test::{protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn returns_api_version() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn returns_api_version() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::version();
-	let response = service.fetch_json::<_, dto::Version>(&request);
+	let response = service.fetch_json::<_, dto::Version>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn initial_setup_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn initial_setup_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::initial_setup();
 	{
-		let response = service.fetch_json::<_, dto::InitialSetup>(&request);
+		let response = service.fetch_json::<_, dto::InitialSetup>(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 		let initial_setup = response.body();
 		assert_eq!(
@@ -28,9 +28,9 @@ fn initial_setup_golden_path() {
 			}
 		);
 	}
-	service.complete_initial_setup();
+	service.complete_initial_setup().await;
 	{
-		let response = service.fetch_json::<_, dto::InitialSetup>(&request);
+		let response = service.fetch_json::<_, dto::InitialSetup>(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 		let initial_setup = response.body();
 		assert_eq!(
@@ -42,40 +42,44 @@ fn initial_setup_golden_path() {
 	}
 }
 
-#[test]
-fn trigger_index_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn trigger_index_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let request = protocol::random();
 
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	let entries = response.body();
 	assert_eq!(entries.len(), 0);
 
-	service.index();
+	service.index().await;
 
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	let entries = response.body();
 	assert_eq!(entries.len(), 3);
 }
 
-#[test]
-fn trigger_index_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn trigger_index_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::trigger_index();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn trigger_index_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn trigger_index_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 	let request = protocol::trigger_index();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
diff --git a/src/service/test/auth.rs b/src/service/test/auth.rs
index 70fb278..852dd3b 100644
--- a/src/service/test/auth.rs
+++ b/src/service/test/auth.rs
@@ -5,33 +5,33 @@ use crate::service::dto;
 use crate::service::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn login_rejects_bad_username() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn login_rejects_bad_username() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let request = protocol::login("garbage", TEST_PASSWORD);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn login_rejects_bad_password() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn login_rejects_bad_password() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let request = protocol::login(TEST_USERNAME, "garbage");
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn login_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn login_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
-	let response = service.fetch_json::<_, dto::Authorization>(&request);
+	let response = service.fetch_json::<_, dto::Authorization>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 
 	let authorization = response.body();
@@ -40,73 +40,73 @@ fn login_golden_path() {
 	assert!(!authorization.token.is_empty());
 }
 
-#[test]
-fn authentication_via_bearer_http_header_rejects_bad_token() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn authentication_via_bearer_http_header_rejects_bad_token() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let mut request = protocol::random();
 	let bearer = headers::Authorization::bearer("garbage").unwrap();
 	request.headers_mut().typed_insert(bearer);
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn authentication_via_bearer_http_header_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn authentication_via_bearer_http_header_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let authorization = {
 		let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
-		let response = service.fetch_json::<_, dto::Authorization>(&request);
+		let response = service.fetch_json::<_, dto::Authorization>(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 		response.into_body()
 	};
 
-	service.logout();
+	service.logout().await;
 
 	let mut request = protocol::random();
 	let bearer = headers::Authorization::bearer(&authorization.token).unwrap();
 	request.headers_mut().typed_insert(bearer);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn authentication_via_query_param_rejects_bad_token() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn authentication_via_query_param_rejects_bad_token() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let mut request = protocol::random();
 	*request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token")
 		.parse()
 		.unwrap();
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn authentication_via_query_param_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn authentication_via_query_param_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let authorization = {
 		let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
-		let response = service.fetch_json::<_, dto::Authorization>(&request);
+		let response = service.fetch_json::<_, dto::Authorization>(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 		response.into_body()
 	};
 
-	service.logout();
+	service.logout().await;
 
 	let mut request = protocol::random();
 	*request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token)
 		.parse()
 		.unwrap();
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
diff --git a/src/service/test/collection.rs b/src/service/test/collection.rs
index a95aa75..0e5e614 100644
--- a/src/service/test/collection.rs
+++ b/src/service/test/collection.rs
@@ -5,214 +5,230 @@ use crate::app::index;
 use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn browse_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn browse_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::browse(&PathBuf::new());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn browse_root() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn browse_root() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::browse(&PathBuf::new());
-	let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::CollectionFile>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 1);
 }
 
-#[test]
-fn browse_directory() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn browse_directory() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
 	let request = protocol::browse(&path);
-	let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::CollectionFile>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 5);
 }
 
-#[test]
-fn browse_bad_directory() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn browse_bad_directory() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
 	let request = protocol::browse(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
-#[test]
-fn flatten_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn flatten_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::flatten(&PathBuf::new());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn flatten_root() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn flatten_root() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::flatten(&PathBuf::new());
-	let response = service.fetch_json::<_, Vec<index::Song>>(&request);
+	let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 13);
 }
 
-#[test]
-fn flatten_directory() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn flatten_directory() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::flatten(Path::new(TEST_MOUNT_NAME));
-	let response = service.fetch_json::<_, Vec<index::Song>>(&request);
+	let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 13);
 }
 
-#[test]
-fn flatten_bad_directory() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn flatten_bad_directory() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
 	let request = protocol::flatten(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
-#[test]
-fn random_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn random_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::random();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn random_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn random_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::random();
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 3);
 }
 
-#[test]
-fn random_with_trailing_slash() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn random_with_trailing_slash() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let mut request = protocol::random();
 	add_trailing_slash(&mut request);
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 3);
 }
 
-#[test]
-fn recent_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn recent_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::recent();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn recent_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn recent_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::recent();
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 3);
 }
 
-#[test]
-fn recent_with_trailing_slash() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn recent_with_trailing_slash() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let mut request = protocol::recent();
 	add_trailing_slash(&mut request);
-	let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::Directory>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let entries = response.body();
 	assert_eq!(entries.len(), 3);
 }
 
-#[test]
-fn search_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn search_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::search("");
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn search_without_query() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn search_without_query() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::search("");
-	let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::CollectionFile>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn search_with_query() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn search_with_query() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let request = protocol::search("door");
-	let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<index::CollectionFile>>(&request)
+		.await;
 	let results = response.body();
 	assert_eq!(results.len(), 1);
 	match results[0] {
diff --git a/src/service/test/ddns.rs b/src/service/test/ddns.rs
index 8c269b5..6ab74aa 100644
--- a/src/service/test/ddns.rs
+++ b/src/service/test/ddns.rs
@@ -4,60 +4,60 @@ use crate::service::dto;
 use crate::service::test::{protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn get_ddns_config_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn get_ddns_config_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::get_ddns_config();
-	service.complete_initial_setup();
+	service.complete_initial_setup().await;
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn get_ddns_config_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn get_ddns_config_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let request = protocol::get_ddns_config();
-	let response = service.fetch_json::<_, dto::DDNSConfig>(&request);
+	let response = service.fetch_json::<_, dto::DDNSConfig>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn put_ddns_config_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn put_ddns_config_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::put_ddns_config(dto::DDNSConfig {
 		host: "test".to_owned(),
 		username: "test".to_owned(),
 		password: "test".to_owned(),
 	});
-	service.complete_initial_setup();
+	service.complete_initial_setup().await;
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn put_ddns_config_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn put_ddns_config_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let request = protocol::put_ddns_config(dto::DDNSConfig {
 		host: "test".to_owned(),
 		username: "test".to_owned(),
 		password: "test".to_owned(),
 	});
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
diff --git a/src/service/test/lastfm.rs b/src/service/test/lastfm.rs
index 7373124..83a8364 100644
--- a/src/service/test/lastfm.rs
+++ b/src/service/test/lastfm.rs
@@ -5,56 +5,58 @@ use crate::service::dto;
 use crate::service::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn lastfm_scrobble_ignores_unlinked_user() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn lastfm_scrobble_ignores_unlinked_user() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
 		.collect();
 
 	let request = protocol::lastfm_scrobble(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NO_CONTENT);
 }
 
-#[test]
-fn lastfm_now_playing_ignores_unlinked_user() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn lastfm_now_playing_ignores_unlinked_user() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
 		.collect();
 
 	let request = protocol::lastfm_now_playing(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NO_CONTENT);
 }
 
-#[test]
-fn lastfm_link_token_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn lastfm_link_token_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::lastfm_link_token();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn lastfm_link_token_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn lastfm_link_token_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::lastfm_link_token();
-	let response = service.fetch_json::<_, dto::LastFMLinkToken>(&request);
+	let response = service
+		.fetch_json::<_, dto::LastFMLinkToken>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let link_token = response.body();
 	assert!(!link_token.value.is_empty());
diff --git a/src/service/test/media.rs b/src/service/test/media.rs
index 86e5833..a6fbd0b 100644
--- a/src/service/test/media.rs
+++ b/src/service/test/media.rs
@@ -5,33 +5,33 @@ use crate::service::dto::ThumbnailSize;
 use crate::service::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn audio_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn audio_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
 		.collect();
 
 	let request = protocol::audio(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn audio_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn audio_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
 		.collect();
 
 	let request = protocol::audio(&path);
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	assert_eq!(response.body().len(), 24_142);
 	assert_eq!(
@@ -40,13 +40,13 @@ fn audio_golden_path() {
 	);
 }
 
-#[test]
-fn audio_does_not_encode_content() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn audio_does_not_encode_content() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
@@ -59,7 +59,7 @@ fn audio_does_not_encode_content() {
 		HeaderValue::from_str("gzip, deflate, br").unwrap(),
 	);
 
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	assert_eq!(response.body().len(), 24_142);
 	assert_eq!(response.headers().get(header::TRANSFER_ENCODING), None);
@@ -69,13 +69,13 @@ fn audio_does_not_encode_content() {
 	);
 }
 
-#[test]
-fn audio_partial_content() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn audio_partial_content() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
 		.iter()
@@ -88,7 +88,7 @@ fn audio_partial_content() {
 		HeaderValue::from_str("bytes=100-299").unwrap(),
 	);
 
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
 	assert_eq!(response.body().len(), 200);
 	assert_eq!(
@@ -97,22 +97,22 @@ fn audio_partial_content() {
 	);
 }
 
-#[test]
-fn audio_bad_path_returns_not_found() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn audio_bad_path_returns_not_found() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
 
 	let request = protocol::audio(&path);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
-#[test]
-fn thumbnail_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn thumbnail_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
 		.iter()
@@ -121,17 +121,17 @@ fn thumbnail_requires_auth() {
 	let size = None;
 	let pad = None;
 	let request = protocol::thumbnail(&path, size, pad);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn thumbnail_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+#[actix_web::test]
+async fn thumbnail_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
 		.iter()
@@ -140,60 +140,60 @@ fn thumbnail_golden_path() {
 	let size = None;
 	let pad = None;
 	let request = protocol::thumbnail(&path, size, pad);
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn thumbnail_bad_path_returns_not_found() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn thumbnail_bad_path_returns_not_found() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let path: PathBuf = ["not_my_collection"].iter().collect();
 
 	let size = None;
 	let pad = None;
 	let request = protocol::thumbnail(&path, size, pad);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
-#[test]
-fn thumbnail_size_default() {
-	thumbnail_size(&test_name!(), None, None, 400);
+#[actix_web::test]
+async fn thumbnail_size_default() {
+	thumbnail_size(&test_name!(), None, None, 400).await;
 }
 
-#[test]
-fn thumbnail_size_small() {
-	thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400);
+#[actix_web::test]
+async fn thumbnail_size_small() {
+	thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400).await;
 }
 
-#[test]
+#[actix_web::test]
 #[cfg(not(tarpaulin))]
-fn thumbnail_size_large() {
-	thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200);
+async fn thumbnail_size_large() {
+	thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200).await;
 }
 
-#[test]
+#[actix_web::test]
 #[cfg(not(tarpaulin))]
-fn thumbnail_size_native() {
-	thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423);
+async fn thumbnail_size_native() {
+	thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423).await;
 }
 
-fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) {
-	let mut service = ServiceType::new(name);
-	service.complete_initial_setup();
-	service.login_admin();
-	service.index();
-	service.login();
+async fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) {
+	let mut service = ServiceType::new(name).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
+	service.index().await;
+	service.login().await;
 
 	let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic", "Folder.png"]
 		.iter()
 		.collect();
 
 	let request = protocol::thumbnail(&path, size, pad);
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 	let thumbnail = image::load_from_memory(response.body()).unwrap().to_rgb8();
 	assert_eq!(thumbnail.width(), expected);
diff --git a/src/service/test/playlist.rs b/src/service/test/playlist.rs
index 3677800..0c5a94f 100644
--- a/src/service/test/playlist.rs
+++ b/src/service/test/playlist.rs
@@ -5,130 +5,132 @@ use crate::service::dto;
 use crate::service::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn list_playlists_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn list_playlists_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::playlists();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn list_playlists_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn list_playlists_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 	let request = protocol::playlists();
-	let response = service.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request);
+	let response = service
+		.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request)
+		.await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn save_playlist_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn save_playlist_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
 	let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn save_playlist_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn save_playlist_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
 	let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn save_playlist_large() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn save_playlist_large() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let tracks = (0..100_000)
 		.map(|_| "My Super Cool Song".to_string())
 		.collect();
 	let my_playlist = dto::SavePlaylistInput { tracks };
 	let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn get_playlist_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn get_playlist_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn get_playlist_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn get_playlist_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	{
 		let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
 		let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
-		let response = service.fetch(&request);
+		let response = service.fetch(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 	}
 
 	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch_json::<_, Vec<index::Song>>(&request);
+	let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn get_playlist_bad_name_returns_not_found() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn get_playlist_bad_name_returns_not_found() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
 
-#[test]
-fn delete_playlist_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn delete_playlist_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn delete_playlist_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn delete_playlist_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	{
 		let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
 		let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
-		let response = service.fetch(&request);
+		let response = service.fetch(&request).await;
 		assert_eq!(response.status(), StatusCode::OK);
 	}
 
 	let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn delete_playlist_bad_name_returns_not_found() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn delete_playlist_bad_name_returns_not_found() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::NOT_FOUND);
 }
diff --git a/src/service/test/settings.rs b/src/service/test/settings.rs
index cde1160..4622eb5 100644
--- a/src/service/test/settings.rs
+++ b/src/service/test/settings.rs
@@ -4,72 +4,72 @@ use crate::service::dto::{self, Settings};
 use crate::service::test::{protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn get_settings_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn get_settings_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 
 	let request = protocol::get_settings();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn get_settings_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn get_settings_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::get_settings();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn get_settings_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn get_settings_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let request = protocol::get_settings();
-	let response = service.fetch_json::<_, dto::Settings>(&request);
+	let response = service.fetch_json::<_, dto::Settings>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn put_settings_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn put_settings_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::put_settings(dto::NewSettings::default());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn put_settings_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn put_settings_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 	let request = protocol::put_settings(dto::NewSettings::default());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn put_settings_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn put_settings_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let request = protocol::put_settings(dto::NewSettings {
 		album_art_pattern: Some("test_pattern".to_owned()),
 		reindex_every_n_seconds: Some(31),
 	});
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 
 	let request = protocol::get_settings();
-	let response = service.fetch_json::<_, dto::Settings>(&request);
+	let response = service.fetch_json::<_, dto::Settings>(&request).await;
 	let settings = response.body();
 	assert_eq!(
 		settings,
diff --git a/src/service/test/swagger.rs b/src/service/test/swagger.rs
index 35bb167..e3ebae2 100644
--- a/src/service/test/swagger.rs
+++ b/src/service/test/swagger.rs
@@ -3,19 +3,19 @@ use http::StatusCode;
 use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn can_get_swagger_index() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn can_get_swagger_index() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::swagger_index();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn can_get_swagger_index_with_trailing_slash() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn can_get_swagger_index_with_trailing_slash() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let mut request = protocol::swagger_index();
 	add_trailing_slash(&mut request);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
diff --git a/src/service/test/user.rs b/src/service/test/user.rs
index 19c15d8..695c917 100644
--- a/src/service/test/user.rs
+++ b/src/service/test/user.rs
@@ -6,53 +6,53 @@ use crate::service::dto;
 use crate::service::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn list_users_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn list_users_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::list_users();
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn list_users_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn list_users_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 	let request = protocol::list_users();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn create_user_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn create_user_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::create_user(dto::NewUser {
 		name: "Walter".into(),
 		password: "secret".into(),
 		admin: false,
 	});
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn create_user_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login_admin();
+#[actix_web::test]
+async fn create_user_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login_admin().await;
 
 	let new_user = dto::NewUser {
 		name: "Walter".into(),
@@ -60,39 +60,39 @@ fn create_user_golden_path() {
 		admin: false,
 	};
 	let request = protocol::create_user(new_user);
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn update_user_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn update_user_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::update_user("Walter", dto::UserUpdate::default());
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn update_user_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn update_user_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::update_user("Walter", dto::UserUpdate::default());
 
-	service.login_admin();
-	let response = service.fetch(&request);
+	service.login_admin().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn update_user_cannot_unadmin_self() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn update_user_cannot_unadmin_self() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::update_user(
 		TEST_USERNAME_ADMIN,
 		dto::UserUpdate {
@@ -101,80 +101,80 @@ fn update_user_cannot_unadmin_self() {
 		},
 	);
 
-	service.login_admin();
-	let response = service.fetch(&request);
+	service.login_admin().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::CONFLICT);
 }
 
-#[test]
-fn delete_user_requires_admin() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn delete_user_requires_admin() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::delete_user("Walter");
 
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 
-	service.login();
-	let response = service.fetch(&request);
+	service.login().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::FORBIDDEN);
 }
 
-#[test]
-fn delete_user_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn delete_user_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::delete_user("Walter");
 
-	service.login_admin();
-	let response = service.fetch(&request);
+	service.login_admin().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn delete_user_cannot_delete_self() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
+#[actix_web::test]
+async fn delete_user_cannot_delete_self() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
 	let request = protocol::delete_user(TEST_USERNAME_ADMIN);
-	service.login_admin();
-	let response = service.fetch(&request);
+	service.login_admin().await;
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::CONFLICT);
 }
 
-#[test]
-fn get_preferences_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn get_preferences_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::get_preferences();
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn get_preferences_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn get_preferences_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::get_preferences();
-	let response = service.fetch_json::<_, user::Preferences>(&request);
+	let response = service.fetch_json::<_, user::Preferences>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
-#[test]
-fn put_preferences_requires_auth() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn put_preferences_requires_auth() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::put_preferences(user::Preferences::default());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
 
-#[test]
-fn put_preferences_golden_path() {
-	let mut service = ServiceType::new(&test_name!());
-	service.complete_initial_setup();
-	service.login();
+#[actix_web::test]
+async fn put_preferences_golden_path() {
+	let mut service = ServiceType::new(&test_name!()).await;
+	service.complete_initial_setup().await;
+	service.login().await;
 
 	let request = protocol::put_preferences(user::Preferences::default());
-	let response = service.fetch(&request);
+	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
diff --git a/src/service/test/web.rs b/src/service/test/web.rs
index 00169d2..5d95288 100644
--- a/src/service/test/web.rs
+++ b/src/service/test/web.rs
@@ -3,10 +3,10 @@ use http::StatusCode;
 use crate::service::test::{protocol, ServiceType, TestService};
 use crate::test_name;
 
-#[test]
-fn serves_web_client() {
-	let mut service = ServiceType::new(&test_name!());
+#[actix_web::test]
+async fn serves_web_client() {
+	let mut service = ServiceType::new(&test_name!()).await;
 	let request = protocol::web_index();
-	let response = service.fetch_bytes(&request);
+	let response = service.fetch_bytes(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
diff --git a/update_db_schema.bat b/update_db_schema.bat
deleted file mode 100644
index 9d17e94..0000000
--- a/update_db_schema.bat
+++ /dev/null
@@ -1,6 +0,0 @@
-cargo install diesel_cli --no-default-features --features "sqlite-bundled"
-
-mkdir tmp
-diesel --database-url tmp/print-schema.sqlite setup
-diesel --database-url tmp/print-schema.sqlite migration run
-rmdir /q /s tmp
\ No newline at end of file