From a89e3d51451d445e773c4266762a332cc2b4f39b Mon Sep 17 00:00:00 2001
From: Antoine Gersant <antoine.gersant@lesforges.org>
Date: Sun, 6 Oct 2024 23:12:57 -0700
Subject: [PATCH] WIP

---
 .gitignore                     |   1 +
 CHANGELOG.md                   |   3 +-
 Cargo.lock                     | 656 ---------------------------------
 Cargo.toml                     |   8 -
 docs/swagger/polaris-api.json  |  50 ---
 flake.nix                      |   4 -
 res/unix/Makefile              |   7 +
 src/app.rs                     |  74 ++--
 src/app/config.rs              | 380 ++++++++++++-------
 src/app/scanner.rs             |  55 +--
 src/app/settings.rs            |  90 -----
 src/app/test.rs                |  66 +---
 src/app/vfs.rs                 | 178 ---------
 src/db.rs                      |  58 ---
 src/db/20240711080449_init.sql |  51 ---
 src/main.rs                    |   1 -
 src/paths.rs                   |  12 +-
 src/server/axum.rs             |  24 --
 src/server/axum/api.rs         |  95 +++--
 src/server/axum/auth.rs        |  10 +-
 src/server/axum/error.rs       |   1 -
 src/server/axum/test.rs        |   2 +-
 src/server/dto/v8.rs           |  90 +----
 src/server/error.rs            |   7 -
 src/server/test/protocol.rs    |  20 +-
 src/server/test/user.rs        |   7 +-
 26 files changed, 414 insertions(+), 1536 deletions(-)
 delete mode 100644 src/app/settings.rs
 delete mode 100644 src/app/vfs.rs
 delete mode 100644 src/db.rs
 delete mode 100644 src/db/20240711080449_init.sql

diff --git a/.gitignore b/.gitignore
index fc4762b..b6aaed4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ TestConfig.toml
 *.sqlite
 **/*.sqlite-shm
 **/*.sqlite-wal
+auth.secret
 collection.index
 polaris.log
 polaris.ndb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2b0e62..fed4cf2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,7 +18,8 @@
 - The `/thumbnail` endpoint supports a new size labeled `tiny` which returns 40x40px images.
 - Persistent data, such as playlists, is now saved in a directory that may be configured with the `--data` CLI option or the `POLARIS_DATA_DIR` environment variable.
 - Removed last.fm integration due to maintenance concerns (abandoned libraries, broken account linking) and mismatch with project goals.
-- Removed `/config` API endpoint.
+- Removed the `/config` API endpoint.
+- Removed the `/ddns` API endpoints, merged into the existing `/settings` endpoints.
 
 ### Web client
 
diff --git a/Cargo.lock b/Cargo.lock
index a859236..e945120 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,15 +89,6 @@ dependencies = [
  "syn 2.0.72",
 ]
 
-[[package]]
-name = "atoi"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
-dependencies = [
- "num-traits",
-]
-
 [[package]]
 name = "auto-future"
 version = "1.0.0"
@@ -314,9 +305,6 @@ name = "bitflags"
 version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "block-buffer"
@@ -427,21 +415,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
-[[package]]
-name = "concurrent-queue"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "const-oid"
-version = "0.9.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
-
 [[package]]
 name = "cookie"
 version = "0.18.1"
@@ -461,21 +434,6 @@ 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.4.2"
@@ -504,15 +462,6 @@ dependencies = [
  "crossbeam-utils",
 ]
 
-[[package]]
-name = "crossbeam-queue"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
-dependencies = [
- "crossbeam-utils",
-]
-
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.20"
@@ -544,17 +493,6 @@ 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.11"
@@ -577,25 +515,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
- "const-oid",
  "crypto-common",
  "subtle",
 ]
 
-[[package]]
-name = "dotenvy"
-version = "0.15.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
-
 [[package]]
 name = "either"
 version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "embed-resource"
@@ -666,28 +594,6 @@ dependencies = [
  "version_check",
 ]
 
-[[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 = "5.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
-dependencies = [
- "concurrent-queue",
- "parking",
- "pin-project-lite",
-]
-
 [[package]]
 name = "extended"
 version = "0.1.0"
@@ -735,17 +641,6 @@ 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",
-]
-
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -791,28 +686,6 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
 
-[[package]]
-name = "futures-executor"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
-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"
@@ -917,15 +790,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "hashlink"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
-dependencies = [
- "hashbrown",
-]
-
 [[package]]
 name = "headers"
 version = "0.4.0"
@@ -950,12 +814,6 @@ dependencies = [
  "http 1.1.0",
 ]
 
-[[package]]
-name = "heck"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-
 [[package]]
 name = "hermit-abi"
 version = "0.3.9"
@@ -968,15 +826,6 @@ 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"
@@ -986,15 +835,6 @@ 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.12"
@@ -1166,9 +1006,6 @@ name = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-dependencies = [
- "spin",
-]
 
 [[package]]
 name = "lewton"
@@ -1187,39 +1024,12 @@ version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
-[[package]]
-name = "libm"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
-
-[[package]]
-name = "libsqlite3-sys"
-version = "0.28.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
-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 = "lock_api"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
 [[package]]
 name = "log"
 version = "0.4.22"
@@ -1242,16 +1052,6 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
 
-[[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 = "memchr"
 version = "2.7.4"
@@ -1284,12 +1084,6 @@ 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.4"
@@ -1418,33 +1212,6 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
 
-[[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-complex"
 version = "0.4.6"
@@ -1469,17 +1236,6 @@ dependencies = [
  "num-traits",
 ]
 
-[[package]]
-name = "num-iter"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -1487,7 +1243,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
- "libm",
 ]
 
 [[package]]
@@ -1552,35 +1307,6 @@ dependencies = [
  "zeroize",
 ]
 
-[[package]]
-name = "parking"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall 0.5.3",
- "smallvec",
- "windows-targets 0.52.6",
-]
-
 [[package]]
 name = "password-hash"
 version = "0.4.2"
@@ -1592,12 +1318,6 @@ dependencies = [
  "subtle",
 ]
 
-[[package]]
-name = "paste"
-version = "1.0.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
-
 [[package]]
 name = "pbkdf2"
 version = "0.11.0"
@@ -1610,15 +1330,6 @@ 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.1"
@@ -1657,33 +1368,6 @@ 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.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
-
 [[package]]
 name = "png"
 version = "0.17.13"
@@ -1742,7 +1426,6 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "simplelog",
- "sqlx",
  "symphonia",
  "thiserror",
  "tinyvec",
@@ -1906,24 +1589,6 @@ dependencies = [
  "libc",
 ]
 
-[[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 = "redox_syscall"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
-dependencies = [
- "bitflags 2.6.0",
-]
-
 [[package]]
 name = "regex"
 version = "1.10.6"
@@ -1978,26 +1643,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[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 = "rust-multipart-rfc7578_2"
 version = "0.6.1"
@@ -2110,12 +1755,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
 [[package]]
 name = "sd-notify"
 version = "0.4.2"
@@ -2216,16 +1855,6 @@ dependencies = [
  "digest",
 ]
 
-[[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"
@@ -2272,9 +1901,6 @@ name = "smallvec"
 version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "socket2"
@@ -2291,220 +1917,6 @@ 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.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c"
-dependencies = [
- "sqlx-core",
- "sqlx-macros",
- "sqlx-mysql",
- "sqlx-postgres",
- "sqlx-sqlite",
-]
-
-[[package]]
-name = "sqlx-core"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b"
-dependencies = [
- "atoi",
- "byteorder",
- "bytes",
- "crc",
- "crossbeam-queue",
- "either",
- "event-listener",
- "futures-channel",
- "futures-core",
- "futures-intrusive",
- "futures-io",
- "futures-util",
- "hashbrown",
- "hashlink",
- "hex",
- "indexmap",
- "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.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88"
-dependencies = [
- "proc-macro2",
- "quote",
- "sqlx-core",
- "sqlx-macros-core",
- "syn 2.0.72",
-]
-
-[[package]]
-name = "sqlx-macros-core"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776"
-dependencies = [
- "dotenvy",
- "either",
- "heck",
- "hex",
- "once_cell",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "sha2",
- "sqlx-core",
- "sqlx-sqlite",
- "syn 2.0.72",
- "tempfile",
- "tokio",
- "url",
-]
-
-[[package]]
-name = "sqlx-mysql"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6"
-dependencies = [
- "atoi",
- "base64 0.22.1",
- "bitflags 2.6.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",
- "sha1",
- "sha2",
- "smallvec",
- "sqlx-core",
- "stringprep",
- "thiserror",
- "tracing",
- "whoami",
-]
-
-[[package]]
-name = "sqlx-postgres"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21"
-dependencies = [
- "atoi",
- "base64 0.22.1",
- "bitflags 2.6.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.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee"
-dependencies = [
- "atoi",
- "flume",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-intrusive",
- "futures-util",
- "libsqlite3-sys",
- "log",
- "percent-encoding",
- "serde",
- "serde_urlencoded",
- "sqlx-core",
- "tracing",
- "url",
-]
 
 [[package]]
 name = "stacker"
@@ -2525,17 +1937,6 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
 
-[[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.6.1"
@@ -2890,17 +2291,6 @@ dependencies = [
  "syn 2.0.72",
 ]
 
-[[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]]
 name = "tokio-util"
 version = "0.7.11"
@@ -3035,21 +2425,9 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
 dependencies = [
  "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.72",
-]
-
 [[package]]
 name = "tracing-core"
 version = "0.1.32"
@@ -3121,24 +2499,12 @@ 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-width"
 version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
 
-[[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.9.0"
@@ -3171,12 +2537,6 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "vcpkg"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
-
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -3228,12 +2588,6 @@ 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 = "webpki-roots"
 version = "0.26.3"
@@ -3249,16 +2603,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
 
-[[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"
diff --git a/Cargo.toml b/Cargo.toml
index 7263127..d21a2b6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,11 +70,6 @@ version = "0.25.2"
 default-features = false
 features = ["bmp", "gif", "jpeg", "png"]
 
-[dependencies.sqlx]
-version = "0.8.0"
-default-features = false
-features = ["macros", "migrate", "runtime-tokio", "sqlite"]
-
 [target.'cfg(windows)'.dependencies]
 native-windows-gui = { version = "1.0.13", default-features = false, features = [
 	"cursor",
@@ -97,6 +92,3 @@ winres = "0.1"
 axum-test = "15.7"
 bytes = "1.7.1"
 percent-encoding = "2.2"
-
-[profile.dev.package.sqlx-macros]
-opt-level = 3
diff --git a/docs/swagger/polaris-api.json b/docs/swagger/polaris-api.json
index 7186dad..5b45c50 100644
--- a/docs/swagger/polaris-api.json
+++ b/docs/swagger/polaris-api.json
@@ -99,36 +99,6 @@
                 ]
             }
         },
-        "/config": {
-            "put": {
-                "tags": [
-                    "Configuration"
-                ],
-                "summary": "Amends the server settings, mount directories and list of users",
-                "operationId": "putConfig",
-                "requestBody": {
-                    "required": true,
-                    "content": {
-                        "application/json": {
-                            "schema": {
-                                "$ref": "#components/schemas/Config"
-                            }
-                        }
-                    }
-                },
-                "responses": {
-                    "200": {
-                        "description": "Successful operation"
-                    }
-                },
-                "security": [
-                    {
-                        "admin_http_bearer": [],
-                        "admin_query_parameter": []
-                    }
-                ]
-            }
-        },
         "/mount_dirs": {
             "get": {
                 "tags": [
@@ -1020,26 +990,6 @@
                     }
                 }
             },
-            "Config": {
-                "type": "object",
-                "properties": {
-                    "settings": {
-                        "$ref": "#/components/schemas/Settings"
-                    },
-                    "mount_dirs": {
-                        "type": "array",
-                        "items": {
-                            "$ref": "#/components/schemas/MountDir"
-                        }
-                    },
-                    "users": {
-                        "type": "array",
-                        "items": {
-                            "$ref": "#/components/schemas/User"
-                        }
-                    }
-                }
-            },
             "User": {
                 "type": "object",
                 "properties": {
diff --git a/flake.nix b/flake.nix
index 93ce75e..a6140c8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -45,16 +45,12 @@
             cargo-edit
             cargo-watch
             rust-analyzer
-            sqlx-cli
             samply
-            sqlitebrowser
           ];
 
           env = {
             # Required by rust-analyzer
             RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
-            # SQLx dev database URL
-            DATABASE_URL=sqlite:./src/db/schema.sqlite;
           };
         };
       });
diff --git a/res/unix/Makefile b/res/unix/Makefile
index f19719c..8471fef 100644
--- a/res/unix/Makefile
+++ b/res/unix/Makefile
@@ -7,11 +7,13 @@ EXEC_PREFIX ?= $(PREFIX)
 BINDIR ?= $(EXEC_PREFIX)/bin
 DATAROOTDIR ?= $(PREFIX)/share
 DATADIR ?= $(DATAROOTDIR)
+SYSCONFDIR ?= $(PREFIX)/etc
 LOCALSTATEDIR ?= $(PREFIX)/var
 RUNSTATEDIR ?= $(LOCALSTATEDIR)/run
 %-system: POLARIS_BIN_PATH := $(BINDIR)/polaris
 %-system: export POLARIS_WEB_DIR := $(DATADIR)/polaris/web
 %-system: export POLARIS_SWAGGER_DIR := $(DATADIR)/polaris/swagger
+%-system: export POLARIS_CONFIG_DIR := $(SYSCONFDIR)/polaris
 %-system: export POLARIS_DATA_DIR := $(LOCALSTATEDIR)/lib/polaris
 %-system: export POLARIS_DB_DIR := $(LOCALSTATEDIR)/lib/polaris
 %-system: export POLARIS_LOG_DIR := $(LOCALSTATEDIR)/log/polaris
@@ -19,10 +21,12 @@ RUNSTATEDIR ?= $(LOCALSTATEDIR)/run
 %-system: export POLARIS_PID_DIR := $(RUNSTATEDIR)/polaris
 
 XDG_CACHE_HOME ?= $(HOME)/.cache
+XDG_CONFIG_HOME ?= $(HOME)/.config
 XDG_DATA_HOME ?= $(HOME)/.local/share
 XDG_BINDIR ?= $(HOME)/.local/bin
 XDG_DATADIR ?= $(XDG_DATA_HOME)/polaris
 XDG_CACHEDIR ?= $(XDG_CACHE_HOME)/polaris
+XDG_CONFIGDIR ?= $(XDG_CONFIG_HOME)/polaris
 ifdef $(XDG_RUNTIME_DIR)
 XDG_PIDDIR ?= $(XDG_RUNTIME_DIR)/polaris
 else
@@ -31,6 +35,7 @@ endif
 %-xdg: POLARIS_BIN_PATH := $(XDG_BINDIR)/polaris
 %-xdg: export POLARIS_WEB_DIR := $(XDG_DATADIR)/web
 %-xdg: export POLARIS_SWAGGER_DIR := $(XDG_DATADIR)/swagger
+%-xdg: export POLARIS_CONFIG_DIR := $(XDG_CONFIGDIR)
 %-xdg: export POLARIS_DATA_DIR := $(XDG_DATADIR)
 %-xdg: export POLARIS_DB_DIR := $(XDG_DATADIR)
 %-xdg: export POLARIS_LOG_DIR := $(XDG_CACHEDIR)
@@ -60,6 +65,7 @@ list-paths:
 	$(info POLARIS_BIN_PATH is $(POLARIS_BIN_PATH))
 	$(info POLARIS_WEB_DIR is $(POLARIS_WEB_DIR))
 	$(info POLARIS_SWAGGER_DIR is $(POLARIS_SWAGGER_DIR))
+	$(info POLARIS_CONFIG_DIR is $(POLARIS_CONFIG_DIR))
 	$(info POLARIS_DATA_DIR is $(POLARIS_DATA_DIR))
 	$(info POLARIS_DB_DIR is $(POLARIS_DB_DIR))
 	$(info POLARIS_LOG_DIR is $(POLARIS_LOG_DIR))
@@ -93,6 +99,7 @@ uninstall-bin:
 uninstall-data:
 	rm -rf $(POLARIS_WEB_DIR)
 	rm -rf $(POLARIS_SWAGGER_DIR)
+	rm -rf $(POLARIS_CONFIG_DIR)
 	rm -rf $(POLARIS_DATA_DIR)
 	rm -rf $(POLARIS_DB_DIR)
 	rm -rf $(POLARIS_LOG_DIR)
diff --git a/src/app.rs b/src/app.rs
index 67556f4..ba4b763 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,7 +1,10 @@
 use std::fs;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+
+use config::AuthSecret;
+use rand::rngs::OsRng;
+use rand::RngCore;
 
-use crate::db::DB;
 use crate::paths::Paths;
 
 pub mod config;
@@ -12,10 +15,8 @@ pub mod ndb;
 pub mod peaks;
 pub mod playlist;
 pub mod scanner;
-pub mod settings;
 pub mod thumbnail;
 pub mod user;
-pub mod vfs;
 
 #[cfg(test)]
 pub mod test;
@@ -64,15 +65,6 @@ pub enum Error {
 	#[error(transparent)]
 	PeaksDeserialization(bitcode::Error),
 
-	#[error(transparent)]
-	Database(#[from] sqlx::Error),
-	#[error("Could not initialize database connection pool")]
-	ConnectionPoolBuild,
-	#[error("Could not acquire database connection from pool")]
-	ConnectionPool,
-	#[error("Could not apply database migrations: {0}")]
-	Migration(sqlx::migrate::MigrateError),
-
 	#[error(transparent)]
 	NativeDatabase(#[from] native_db::db_type::Error),
 	#[error("Could not initialize database")]
@@ -148,19 +140,13 @@ pub struct App {
 	pub scanner: scanner::Scanner,
 	pub index_manager: index::Manager,
 	pub config_manager: config::Manager,
-	pub ddns_manager: ddns::Manager,
 	pub peaks_manager: peaks::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,
 }
 
 impl App {
 	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.data_dir_path)
 			.map_err(|e| Error::Io(paths.data_dir_path.clone(), e))?;
 
@@ -177,34 +163,17 @@ impl App {
 		fs::create_dir_all(&thumbnails_dir_path)
 			.map_err(|e| Error::Io(thumbnails_dir_path.clone(), e))?;
 
+		let auth_secret_file_path = paths.data_dir_path.join("auth.secret");
+		let auth_secret = Self::get_or_create_auth_secret(&auth_secret_file_path);
+
+		let config_manager = config::Manager::new(&paths.config_file_path).await?;
 		let ndb_manager = ndb::Manager::new(&paths.data_dir_path)?;
-		let vfs_manager = vfs::Manager::new(db.clone());
-		let settings_manager = settings::Manager::new(db.clone());
-		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_manager = index::Manager::new(&paths.data_dir_path).await?;
-		let scanner = scanner::Scanner::new(
-			index_manager.clone(),
-			settings_manager.clone(),
-			vfs_manager.clone(),
-		)
-		.await?;
-		let config_manager = config::Manager::new(
-			settings_manager.clone(),
-			user_manager.clone(),
-			vfs_manager.clone(),
-			ddns_manager.clone(),
-		);
+		let scanner = scanner::Scanner::new(index_manager.clone(), config_manager.clone()).await?;
 		let peaks_manager = peaks::Manager::new(peaks_dir_path);
 		let playlist_manager = playlist::Manager::new(ndb_manager);
 		let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
 
-		if let Some(config_path) = paths.config_file_path {
-			let config = config::Config::from_path(&config_path)?;
-			config_manager.apply(&config).await?;
-		}
-
 		Ok(Self {
 			port,
 			web_dir_path: paths.web_dir_path,
@@ -212,13 +181,28 @@ impl App {
 			scanner,
 			index_manager,
 			config_manager,
-			ddns_manager,
 			peaks_manager,
 			playlist_manager,
-			settings_manager,
 			thumbnail_manager,
-			user_manager,
-			vfs_manager,
 		})
 	}
+
+	async fn get_or_create_auth_secret(path: &Path) -> Result<config::AuthSecret, Error> {
+		match tokio::fs::read(&path).await {
+			Ok(s) => Ok(config::AuthSecret {
+				key: s
+					.try_into()
+					.map_err(|_| Error::AuthenticationSecretInvalid)?,
+			}),
+			Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
+				let mut secret = AuthSecret::default();
+				OsRng.fill_bytes(&mut secret.key);
+				tokio::fs::write(&path, &secret.key)
+					.await
+					.map_err(|_| Error::AuthenticationSecretInvalid)?;
+				Ok(secret)
+			}
+			Err(e) => return Err(Error::Io(path.to_owned(), e)),
+		}
+	}
 }
diff --git a/src/app/config.rs b/src/app/config.rs
index d0e9ff2..cc867fd 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -1,14 +1,57 @@
-use serde::Deserialize;
-use std::{io::Read, path::Path};
+use std::{
+	io::Read,
+	ops::Deref,
+	path::{Path, PathBuf},
+	sync::Arc,
+	time::Duration,
+};
 
-use crate::app::{ddns, settings, user, vfs, Error};
+use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
+use pbkdf2::Pbkdf2;
+use rand::rngs::OsRng;
+use regex::Regex;
+use serde::{Deserialize, Serialize};
 
-#[derive(Default, Deserialize)]
+use crate::app::Error;
+
+#[derive(Clone, Default)]
+pub struct AuthSecret {
+	pub key: [u8; 32],
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub struct MountDir {
+	pub source: String,
+	pub name: String,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub struct User {
+	pub name: String,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub admin: Option<bool>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub initial_password: Option<String>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub hashed_password: Option<String>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub web_theme_base: Option<String>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub web_theme_accent: Option<String>,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
 pub struct Config {
-	pub settings: Option<settings::NewSettings>,
-	pub mount_dirs: Option<Vec<vfs::MountDir>>,
-	pub ydns: Option<ddns::Config>,
-	pub users: Option<Vec<user::NewUser>>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub reindex_every_n_seconds: Option<u64>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub album_art_pattern: Option<String>,
+	#[serde(skip_serializing_if = "Vec::is_empty")]
+	pub mount_dirs: Vec<MountDir>,
+	#[serde(skip_serializing_if = "Option::is_none")]
+	pub ddns_url: Option<String>,
+	#[serde(skip_serializing_if = "Vec::is_empty")]
+	pub users: Vec<User>,
 }
 
 impl Config {
@@ -26,72 +69,124 @@ impl Config {
 
 #[derive(Clone)]
 pub struct Manager {
-	settings_manager: settings::Manager,
-	user_manager: user::Manager,
-	vfs_manager: vfs::Manager,
-	ddns_manager: ddns::Manager,
+	config_file_path: PathBuf,
+	config: Arc<tokio::sync::RwLock<Config>>,
 }
 
 impl Manager {
-	pub fn new(
-		settings_manager: settings::Manager,
-		user_manager: user::Manager,
-		vfs_manager: vfs::Manager,
-		ddns_manager: ddns::Manager,
-	) -> Self {
-		Self {
-			settings_manager,
-			user_manager,
-			vfs_manager,
-			ddns_manager,
-		}
+	pub async fn new(config_file_path: &Path) -> Result<Self, Error> {
+		let config = Config::default(); // TODO read from disk!!
+		let manager = Self {
+			config_file_path: config_file_path.to_owned(),
+			config: Arc::default(),
+		};
+		manager.apply(config);
+		Ok(manager)
 	}
 
-	pub async fn apply(&self, config: &Config) -> Result<(), Error> {
-		if let Some(new_settings) = &config.settings {
-			self.settings_manager.amend(new_settings).await?;
-		}
+	pub async fn apply(&self, mut config: Config) -> Result<(), Error> {
+		config
+			.users
+			.retain(|u| u.initial_password.is_some() || u.hashed_password.is_some());
 
-		if let Some(mount_dirs) = &config.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).await?;
-		}
-
-		if let Some(ref users) = config.users {
-			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).await?;
-			}
-
-			// Insert new users
-			for new_user in users
-				.iter()
-				.filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
-			{
-				self.user_manager.create(new_user).await?;
-			}
-
-			// Update users
-			for user in users {
-				self.user_manager
-					.set_password(&user.name, &user.password)
-					.await?;
-				self.user_manager
-					.set_is_admin(&user.name, user.admin)
-					.await?;
+		for user in &mut config.users {
+			if let (Some(password), None) = (&user.initial_password, &user.hashed_password) {
+				user.hashed_password = Some(hash_password(&password)?);
 			}
 		}
 
+		*self.config.write().await = config;
+
+		// TODO persistence
+
 		Ok(())
 	}
+
+	pub async fn get_index_sleep_duration(&self) -> Duration {
+		let seconds = self
+			.config
+			.read()
+			.await
+			.reindex_every_n_seconds
+			.unwrap_or(1800);
+		Duration::from_secs(seconds)
+	}
+
+	pub async fn get_index_album_art_pattern(&self) -> String {
+		self.config
+			.read()
+			.await
+			.album_art_pattern
+			.clone()
+			.unwrap_or("Folder.(jpeg|jpg|png)".to_owned())
+	}
+
+	pub async fn get_ddns_update_url(&self) -> Option<String> {
+		self.config.read().await.ddns_url.clone()
+	}
+
+	pub async fn get_users(&self) -> Vec<User> {
+		self.config.read().await.users.clone()
+	}
+
+	pub async fn get_user(&self, username: &str) -> Result<User, Error> {
+		let config = self.config.read().await;
+		let user = config.users.iter().find(|u| u.name == username);
+		user.cloned().ok_or(Error::UserNotFound)
+	}
+
+	pub async fn delete_user(&self, username: &str) {
+		let mut config = self.config.write().await;
+		config.users.retain(|u| u.name != username);
+		// TODO persistence
+	}
+
+	pub async fn get_mounts(&self) -> Vec<MountDir> {
+		self.config.read().await.mount_dirs.clone()
+	}
+
+	pub async fn resolve_virtual_path<P: AsRef<Path>>(
+		&self,
+		virtual_path: P,
+	) -> Result<PathBuf, Error> {
+		let config = self.config.read().await;
+		for mount in &config.mount_dirs {
+			let mounth_source = sanitize_path(&mount.source);
+			let mount_path = Path::new(&mount.name);
+			if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) {
+				return if p.components().count() == 0 {
+					Ok(mounth_source)
+				} else {
+					Ok(mounth_source.join(p))
+				};
+			}
+		}
+		Err(Error::CouldNotMapToRealPath(virtual_path.as_ref().into()))
+	}
+
+	pub async fn set_mounts(&self, mount_dirs: Vec<MountDir>) {
+		self.config.write().await.mount_dirs = mount_dirs;
+		// TODO persistence
+	}
+}
+
+fn sanitize_path(source: &str) -> PathBuf {
+	let separator_regex = Regex::new(r"\\|/").unwrap();
+	let mut correct_separator = String::new();
+	correct_separator.push(std::path::MAIN_SEPARATOR);
+	let path_string = separator_regex.replace_all(source, correct_separator.as_str());
+	PathBuf::from(path_string.deref())
+}
+
+fn hash_password(password: &str) -> Result<String, Error> {
+	if password.is_empty() {
+		return Err(Error::EmptyPassword);
+	}
+	let salt = SaltString::generate(&mut OsRng);
+	match Pbkdf2.hash_password(password.as_bytes(), &salt) {
+		Ok(h) => Ok(h.to_string()),
+		Err(_) => Err(Error::PasswordHashing),
+	}
 }
 
 #[cfg(test)]
@@ -102,82 +197,121 @@ mod test {
 	use crate::test_name;
 
 	#[tokio::test]
-	async fn apply_saves_misc_settings() {
+	async fn can_apply_config() {
 		let ctx = test::ContextBuilder::new(test_name!()).build().await;
 		let new_config = Config {
-			settings: Some(settings::NewSettings {
-				album_art_pattern: Some("🖼️\\.jpg".into()),
-				reindex_every_n_seconds: Some(100),
-			}),
+			reindex_every_n_seconds: Some(100),
+			album_art_pattern: Some("cool_pattern".to_owned()),
+			mount_dirs: vec![MountDir {
+				source: "/home/music".to_owned(),
+				name: "Library".to_owned(),
+			}],
+			ddns_url: Some("https://cooldns.com".to_owned()),
+			users: vec![],
+		};
+		ctx.config_manager.apply(new_config.clone()).await.unwrap();
+		assert_eq!(new_config, ctx.config_manager.config.read().await.clone(),);
+	}
+
+	#[tokio::test]
+	async fn applying_config_adds_or_preserves_password_hashes() {
+		let ctx = test::ContextBuilder::new(test_name!()).build().await;
+
+		let new_config = Config {
+			users: vec![
+				User {
+					name: "walter".to_owned(),
+					initial_password: Some("super salmon 64".to_owned()),
+					..Default::default()
+				},
+				User {
+					name: "lara".to_owned(),
+					hashed_password: Some("hash".to_owned()),
+					..Default::default()
+				},
+			],
 			..Default::default()
 		};
 
-		ctx.config_manager.apply(&new_config).await.unwrap();
-		let settings = ctx.settings_manager.read().await.unwrap();
-		let new_settings = new_config.settings.unwrap();
+		ctx.config_manager.apply(new_config).await.unwrap();
+		let actual_config = ctx.config_manager.config.read().await.clone();
+
+		assert_eq!(actual_config.users[0].name, "walter");
 		assert_eq!(
-			settings.index_album_art_pattern,
-			new_settings.album_art_pattern.unwrap()
+			actual_config.users[0].initial_password,
+			Some("super salmon 64".to_owned())
 		);
+		assert!(actual_config.users[0].hashed_password.is_some());
+
 		assert_eq!(
-			settings.index_sleep_duration_seconds,
-			new_settings.reindex_every_n_seconds.unwrap()
+			actual_config.users[1],
+			User {
+				name: "lara".to_owned(),
+				hashed_password: Some("hash".to_owned()),
+				..Default::default()
+			}
 		);
 	}
 
-	#[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 {
-				source: "/home/music".into(),
-				name: "🎵📁".into(),
-			}]),
-			..Default::default()
-		};
-
-		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 converts_virtual_to_real() {
+		let vfs = VFS::new(vec![Mount {
+			name: "root".to_owned(),
+			source: Path::new("test_dir").to_owned(),
+		}]);
+		let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
+		let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
+		let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
+		assert_eq!(converted_path, real_path);
 	}
 
-	#[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 {
-				ddns_host: "🐸🐸🐸.ydns.eu".into(),
-				ddns_username: "kfr🐸g".into(),
-				ddns_password: "tasty🐞".into(),
-			}),
-			..Default::default()
-		};
-
-		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 converts_virtual_to_real_top_level() {
+		let vfs = VFS::new(vec![Mount {
+			name: "root".to_owned(),
+			source: Path::new("test_dir").to_owned(),
+		}]);
+		let real_path = Path::new("test_dir");
+		let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap();
+		assert_eq!(converted_path, real_path);
 	}
 
-	#[tokio::test]
-	async fn apply_can_toggle_admin() {
-		let ctx = test::ContextBuilder::new(test_name!())
-			.user("Walter", "Tasty🍖", true)
-			.build()
-			.await;
+	#[test]
+	fn cleans_path_string() {
+		let mut correct_path = path::PathBuf::new();
+		if cfg!(target_os = "windows") {
+			correct_path.push("C:\\");
+		} else {
+			correct_path.push("/usr");
+		}
+		correct_path.push("some");
+		correct_path.push("path");
 
-		assert!(ctx.user_manager.list().await.unwrap()[0].is_admin());
-
-		let new_config = Config {
-			users: Some(vec![user::NewUser {
-				name: "Walter".into(),
-				password: "Tasty🍖".into(),
-				admin: false,
-			}]),
-			..Default::default()
+		let tests = if cfg!(target_os = "windows") {
+			vec![
+				r#"C:/some/path"#,
+				r#"C:\some\path"#,
+				r#"C:\some\path\"#,
+				r#"C:\some\path\\\\"#,
+				r#"C:\some/path//"#,
+			]
+		} else {
+			vec![
+				r#"/usr/some/path"#,
+				r#"/usr\some\path"#,
+				r#"/usr\some\path\"#,
+				r#"/usr\some\path\\\\"#,
+				r#"/usr\some/path//"#,
+			]
 		};
-		ctx.config_manager.apply(&new_config).await.unwrap();
-		assert!(!ctx.user_manager.list().await.unwrap()[0].is_admin());
+
+		for test in tests {
+			let mount_dir = MountDir {
+				source: test.to_owned(),
+				name: "name".to_owned(),
+			};
+			let mount: Mount = mount_dir.into();
+			assert_eq!(mount.source, correct_path);
+		}
 	}
 }
diff --git a/src/app/scanner.rs b/src/app/scanner.rs
index 559f0ae..5f62de2 100644
--- a/src/app/scanner.rs
+++ b/src/app/scanner.rs
@@ -10,7 +10,7 @@ use std::{cmp::min, time::Duration};
 use tokio::sync::Notify;
 use tokio::time::Instant;
 
-use crate::app::{formats, index, settings, vfs, Error};
+use crate::app::{config, formats, index, Error};
 
 #[derive(Debug, PartialEq, Eq)]
 pub struct Directory {
@@ -40,21 +40,18 @@ pub struct Song {
 #[derive(Clone)]
 pub struct Scanner {
 	index_manager: index::Manager,
-	settings_manager: settings::Manager,
-	vfs_manager: vfs::Manager,
+	config_manager: config::Manager,
 	pending_scan: Arc<Notify>,
 }
 
 impl Scanner {
 	pub async fn new(
 		index_manager: index::Manager,
-		settings_manager: settings::Manager,
-		vfs_manager: vfs::Manager,
+		config_manager: config::Manager,
 	) -> Result<Self, Error> {
 		let scanner = Self {
 			index_manager,
-			vfs_manager,
-			settings_manager,
+			config_manager,
 			pending_scan: Arc::new(Notify::new()),
 		};
 
@@ -83,14 +80,7 @@ impl Scanner {
 			async move {
 				loop {
 					index.trigger_scan();
-					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)
-						});
+					let sleep_duration = index.config_manager.get_index_sleep_duration().await;
 					tokio::time::sleep(sleep_duration).await;
 				}
 			}
@@ -104,22 +94,17 @@ impl Scanner {
 		let was_empty = self.index_manager.is_index_empty().await;
 		let mut partial_update_time = Instant::now();
 
-		let album_art_pattern = self
-			.settings_manager
-			.get_index_album_art_pattern()
-			.await
-			.ok();
+		let album_art_pattern = self.config_manager.get_index_album_art_pattern().await;
+		let album_art_regex = Regex::new(&format!("(?i){}", &album_art_pattern)).ok();
 
 		let (scan_directories_output, collection_directories_input) = channel();
 		let (scan_songs_output, collection_songs_input) = channel();
 
-		let vfs = self.vfs_manager.get_vfs().await?;
-
 		let scan = Scan::new(
 			scan_directories_output,
 			scan_songs_output,
-			vfs.mounts().clone(),
-			album_art_pattern,
+			self.config_manager.get_mounts().await,
+			album_art_regex,
 		);
 
 		let scan_task = tokio::task::spawn_blocking(|| scan.run());
@@ -203,7 +188,7 @@ impl Scanner {
 struct Scan {
 	directories_output: Sender<Directory>,
 	songs_output: Sender<Song>,
-	mounts: Vec<vfs::Mount>,
+	mounts: Vec<config::MountDir>,
 	artwork_regex: Option<Regex>,
 }
 
@@ -211,7 +196,7 @@ impl Scan {
 	pub fn new(
 		directories_output: Sender<Directory>,
 		songs_output: Sender<Song>,
-		mounts: Vec<vfs::Mount>,
+		mounts: Vec<config::MountDir>,
 		artwork_regex: Option<Regex>,
 	) -> Self {
 		Self {
@@ -371,9 +356,9 @@ mod test {
 	async fn scan_finds_songs_and_directories() {
 		let (directories_sender, directories_receiver) = channel();
 		let (songs_sender, songs_receiver) = channel();
-		let mounts = vec![vfs::Mount {
-			source: PathBuf::from_iter(["test-data", "small-collection"]),
-			name: "root".to_string(),
+		let mounts = vec![config::MountDir {
+			source: "test-data/small-collection".to_owned(),
+			name: "root".to_owned(),
 		}];
 		let artwork_regex = None;
 
@@ -391,9 +376,9 @@ mod test {
 	async fn scan_finds_embedded_artwork() {
 		let (directories_sender, _) = channel();
 		let (songs_sender, songs_receiver) = channel();
-		let mounts = vec![vfs::Mount {
-			source: PathBuf::from_iter(["test-data", "small-collection"]),
-			name: "root".to_string(),
+		let mounts = vec![config::MountDir {
+			source: "test-data/small-collection".to_owned(),
+			name: "root".to_owned(),
 		}];
 		let artwork_regex = None;
 
@@ -414,9 +399,9 @@ mod test {
 		for pattern in patterns.into_iter() {
 			let (directories_sender, _) = channel();
 			let (songs_sender, songs_receiver) = channel();
-			let mounts = vec![vfs::Mount {
-				source: PathBuf::from_iter(["test-data", "small-collection"]),
-				name: "root".to_string(),
+			let mounts = vec![config::MountDir {
+				source: "test-data/small-collection".to_owned(),
+				name: "root".to_owned(),
 			}];
 			let artwork_regex = Some(Regex::new(pattern).unwrap());
 
diff --git a/src/app/settings.rs b/src/app/settings.rs
deleted file mode 100644
index bd5bb1d..0000000
--- a/src/app/settings.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use regex::Regex;
-use serde::Deserialize;
-use std::time::Duration;
-
-use crate::app::Error;
-use crate::db::DB;
-
-#[derive(Clone, Default)]
-pub struct AuthSecret {
-	pub key: [u8; 32],
-}
-
-#[derive(Debug)]
-pub struct Settings {
-	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<i64>,
-	pub album_art_pattern: Option<String>,
-}
-
-#[derive(Clone)]
-pub struct Manager {
-	pub db: DB,
-}
-
-impl Manager {
-	pub fn new(db: DB) -> Self {
-		Self { db }
-	}
-
-	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 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 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 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 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 {
-			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 {
-			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 0457d74..6f888a3 100644
--- a/src/app/test.rs
+++ b/src/app/test.rs
@@ -1,18 +1,15 @@
 use std::path::PathBuf;
 
-use crate::app::{config, ddns, index, ndb, playlist, scanner, settings, user, vfs};
-use crate::db::DB;
+use crate::app::{config, index, ndb, playlist, scanner};
 use crate::test::*;
 
+use super::config::AuthSecret;
+
 pub struct Context {
 	pub index_manager: index::Manager,
 	pub scanner: scanner::Scanner,
 	pub config_manager: config::Manager,
-	pub ddns_manager: ddns::Manager,
 	pub playlist_manager: playlist::Manager,
-	pub settings_manager: settings::Manager,
-	pub user_manager: user::Manager,
-	pub vfs_manager: vfs::Manager,
 }
 
 pub struct ContextBuilder {
@@ -29,64 +26,41 @@ impl ContextBuilder {
 	}
 
 	pub fn user(mut self, name: &str, password: &str, is_admin: bool) -> Self {
-		self.config
-			.users
-			.get_or_insert(Vec::new())
-			.push(user::NewUser {
-				name: name.to_owned(),
-				password: password.to_owned(),
-				admin: is_admin,
-			});
+		self.config.users.push(config::User {
+			name: name.to_owned(),
+			initial_password: Some(password.to_owned()),
+			admin: Some(is_admin),
+			..Default::default()
+		});
 		self
 	}
 
 	pub fn mount(mut self, name: &str, source: &str) -> Self {
-		self.config
-			.mount_dirs
-			.get_or_insert(Vec::new())
-			.push(vfs::MountDir {
-				name: name.to_owned(),
-				source: source.to_owned(),
-			});
+		self.config.mount_dirs.push(config::MountDir {
+			name: name.to_owned(),
+			source: source.to_owned(),
+		});
 		self
 	}
 	pub async fn build(self) -> Context {
-		let db_path = self.test_directory.join("db.sqlite");
+		let config_path = self.test_directory.join("polaris.toml");
 
-		let db = DB::new(&db_path).await.unwrap();
+		let auth_secret = AuthSecret::default();
+		let config_manager = config::Manager::new(&config_path).await.unwrap();
 		let ndb_manager = ndb::Manager::new(&self.test_directory).unwrap();
-		let settings_manager = settings::Manager::new(db.clone());
-		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());
-		let config_manager = config::Manager::new(
-			settings_manager.clone(),
-			user_manager.clone(),
-			vfs_manager.clone(),
-			ddns_manager.clone(),
-		);
 		let index_manager = index::Manager::new(&self.test_directory).await.unwrap();
-		let scanner = scanner::Scanner::new(
-			index_manager.clone(),
-			settings_manager.clone(),
-			vfs_manager.clone(),
-		)
-		.await
-		.unwrap();
+		let scanner = scanner::Scanner::new(index_manager.clone(), config_manager.clone())
+			.await
+			.unwrap();
 		let playlist_manager = playlist::Manager::new(ndb_manager.clone());
 
-		config_manager.apply(&self.config).await.unwrap();
+		config_manager.apply(self.config).await.unwrap();
 
 		Context {
 			index_manager,
 			scanner,
 			config_manager,
-			ddns_manager,
 			playlist_manager,
-			settings_manager,
-			user_manager,
-			vfs_manager,
 		}
 	}
 }
diff --git a/src/app/vfs.rs b/src/app/vfs.rs
deleted file mode 100644
index 8eb5006..0000000
--- a/src/app/vfs.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-use core::ops::Deref;
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use sqlx::{Acquire, QueryBuilder, Sqlite};
-use std::path::{self, Path, PathBuf};
-
-use crate::app::Error;
-use crate::db::DB;
-
-#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
-pub struct MountDir {
-	pub source: String,
-	pub name: String,
-}
-
-#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
-pub struct Mount {
-	pub source: PathBuf,
-	pub name: String,
-}
-
-impl From<MountDir> for Mount {
-	fn from(m: MountDir) -> Self {
-		let separator_regex = Regex::new(r"\\|/").unwrap();
-		let mut correct_separator = String::new();
-		correct_separator.push(path::MAIN_SEPARATOR);
-		let path_string = separator_regex.replace_all(&m.source, correct_separator.as_str());
-		let source = PathBuf::from(path_string.deref());
-		Self {
-			name: m.name,
-			source,
-		}
-	}
-}
-
-#[allow(clippy::upper_case_acronyms)]
-pub struct VFS {
-	mounts: Vec<Mount>,
-}
-
-impl VFS {
-	pub fn new(mounts: Vec<Mount>) -> VFS {
-		VFS { mounts }
-	}
-
-	pub fn virtual_to_real<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf, Error> {
-		for mount in &self.mounts {
-			let mount_path = Path::new(&mount.name);
-			if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) {
-				return if p.components().count() == 0 {
-					Ok(mount.source.clone())
-				} else {
-					Ok(mount.source.join(p))
-				};
-			}
-		}
-		Err(Error::CouldNotMapToRealPath(virtual_path.as_ref().into()))
-	}
-
-	pub fn mounts(&self) -> &Vec<Mount> {
-		&self.mounts
-	}
-}
-
-#[derive(Clone)]
-pub struct Manager {
-	db: DB,
-}
-
-impl Manager {
-	pub fn new(db: DB) -> Self {
-		Self { db }
-	}
-
-	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 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 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(())
-	}
-}
-
-#[cfg(test)]
-mod test {
-
-	use super::*;
-
-	#[test]
-	fn converts_virtual_to_real() {
-		let vfs = VFS::new(vec![Mount {
-			name: "root".to_owned(),
-			source: Path::new("test_dir").to_owned(),
-		}]);
-		let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
-		let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
-		let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
-		assert_eq!(converted_path, real_path);
-	}
-
-	#[test]
-	fn converts_virtual_to_real_top_level() {
-		let vfs = VFS::new(vec![Mount {
-			name: "root".to_owned(),
-			source: Path::new("test_dir").to_owned(),
-		}]);
-		let real_path = Path::new("test_dir");
-		let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap();
-		assert_eq!(converted_path, real_path);
-	}
-
-	#[test]
-	fn cleans_path_string() {
-		let mut correct_path = path::PathBuf::new();
-		if cfg!(target_os = "windows") {
-			correct_path.push("C:\\");
-		} else {
-			correct_path.push("/usr");
-		}
-		correct_path.push("some");
-		correct_path.push("path");
-
-		let tests = if cfg!(target_os = "windows") {
-			vec![
-				r#"C:/some/path"#,
-				r#"C:\some\path"#,
-				r#"C:\some\path\"#,
-				r#"C:\some\path\\\\"#,
-				r#"C:\some/path//"#,
-			]
-		} else {
-			vec![
-				r#"/usr/some/path"#,
-				r#"/usr\some\path"#,
-				r#"/usr\some\path\"#,
-				r#"/usr\some\path\\\\"#,
-				r#"/usr\some/path//"#,
-			]
-		};
-
-		for test in tests {
-			let mount_dir = MountDir {
-				source: test.to_owned(),
-				name: "name".to_owned(),
-			};
-			let mount: Mount = mount_dir.into();
-			assert_eq!(mount.source, correct_path);
-		}
-	}
-}
diff --git a/src/db.rs b/src/db.rs
deleted file mode 100644
index b5c5706..0000000
--- a/src/db.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-use std::path::Path;
-
-use sqlx::{
-	migrate::Migrator,
-	pool::PoolConnection,
-	sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
-	Sqlite,
-};
-
-use crate::app::Error;
-
-static MIGRATOR: Migrator = sqlx::migrate!("src/db");
-
-#[derive(Clone)]
-pub struct DB {
-	pool: SqlitePool,
-}
-
-impl DB {
-	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 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().await?;
-		Ok(db)
-	}
-
-	pub async fn connect(&self) -> Result<PoolConnection<Sqlite>, Error> {
-		self.pool.acquire().await.map_err(|_| Error::ConnectionPool)
-	}
-
-	async fn migrate_up(&self) -> Result<(), Error> {
-		MIGRATOR
-			.run(&self.pool)
-			.await
-			.and(Ok(()))
-			.map_err(Error::Migration)
-	}
-}
-
-#[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).await.unwrap();
-	db.migrate_up().await.unwrap();
-}
diff --git a/src/db/20240711080449_init.sql b/src/db/20240711080449_init.sql
deleted file mode 100644
index a6eb629..0000000
--- a/src/db/20240711080449_init.sql
+++ /dev/null
@@ -1,51 +0,0 @@
-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,
-	web_theme_base TEXT,
-	web_theme_accent TEXT,
-	UNIQUE(name)
-);
-
-CREATE TABLE collection_index (
-	id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
-	content BLOB
-);
-
-INSERT INTO collection_index (id, content) VALUES (0, NULL);
diff --git a/src/main.rs b/src/main.rs
index 6b50451..cd2dc43 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,7 +10,6 @@ use std::fs;
 use std::path::{Path, PathBuf};
 
 mod app;
-mod db;
 mod options;
 mod paths;
 mod server;
diff --git a/src/paths.rs b/src/paths.rs
index d2133c9..70e1cda 100644
--- a/src/paths.rs
+++ b/src/paths.rs
@@ -4,7 +4,7 @@ use crate::options::CLIOptions;
 
 pub struct Paths {
 	pub cache_dir_path: PathBuf,
-	pub config_file_path: Option<PathBuf>,
+	pub config_file_path: PathBuf,
 	pub data_dir_path: PathBuf,
 	pub db_file_path: PathBuf,
 	pub log_file_path: Option<PathBuf>,
@@ -21,7 +21,7 @@ impl Default for Paths {
 	fn default() -> Self {
 		Self {
 			cache_dir_path: ["."].iter().collect(),
-			config_file_path: None,
+			config_file_path: [".", "polaris.toml"].iter().collect(),
 			data_dir_path: ["."].iter().collect(),
 			db_file_path: [".", "db.sqlite"].iter().collect(),
 			log_file_path: Some([".", "polaris.log"].iter().collect()),
@@ -40,7 +40,7 @@ impl Default for Paths {
 			local_app_data.join(["Permafrost", "Polaris"].iter().collect::<PathBuf>());
 		Self {
 			cache_dir_path: install_directory.clone(),
-			config_file_path: None,
+			config_file_path: install_directory.join("polaris.toml"),
 			data_dir_path: install_directory.clone(),
 			db_file_path: install_directory.join("db.sqlite"),
 			log_file_path: Some(install_directory.join("polaris.log")),
@@ -58,10 +58,12 @@ impl Paths {
 				.map(PathBuf::from)
 				.map(|p| p.join("db.sqlite"))
 				.unwrap_or(defaults.db_file_path),
-			config_file_path: None,
 			cache_dir_path: option_env!("POLARIS_CACHE_DIR")
 				.map(PathBuf::from)
 				.unwrap_or(defaults.cache_dir_path),
+			config_file_path: option_env!("POLARIS_CONFIG_DIR")
+				.map(|p| [p, "polaris.toml"].iter().collect())
+				.unwrap_or(defaults.config_file_path),
 			data_dir_path: option_env!("POLARIS_DATA_DIR")
 				.map(PathBuf::from)
 				.unwrap_or(defaults.data_dir_path),
@@ -89,7 +91,7 @@ impl Paths {
 			path.clone_into(&mut paths.cache_dir_path);
 		}
 		if let Some(path) = &cli_options.config_file_path {
-			paths.config_file_path = Some(path.clone());
+			path.clone_into(&mut paths.config_file_path);
 		}
 		if let Some(path) = &cli_options.data_dir_path {
 			path.clone_into(&mut paths.data_dir_path);
diff --git a/src/server/axum.rs b/src/server/axum.rs
index dedc3cb..eca74c2 100644
--- a/src/server/axum.rs
+++ b/src/server/axum.rs
@@ -59,12 +59,6 @@ impl FromRef<App> for app::config::Manager {
 	}
 }
 
-impl FromRef<App> for app::ddns::Manager {
-	fn from_ref(app: &App) -> Self {
-		app.ddns_manager.clone()
-	}
-}
-
 impl FromRef<App> for app::peaks::Manager {
 	fn from_ref(app: &App) -> Self {
 		app.peaks_manager.clone()
@@ -77,26 +71,8 @@ impl FromRef<App> for app::playlist::Manager {
 	}
 }
 
-impl FromRef<App> for app::user::Manager {
-	fn from_ref(app: &App) -> Self {
-		app.user_manager.clone()
-	}
-}
-
-impl FromRef<App> for app::settings::Manager {
-	fn from_ref(app: &App) -> Self {
-		app.settings_manager.clone()
-	}
-}
-
 impl FromRef<App> for app::thumbnail::Manager {
 	fn from_ref(app: &App) -> Self {
 		app.thumbnail_manager.clone()
 	}
 }
-
-impl FromRef<App> for app::vfs::Manager {
-	fn from_ref(app: &App) -> Self {
-		app.vfs_manager.clone()
-	}
-}
diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs
index e39ca65..27c2ff7 100644
--- a/src/server/axum/api.rs
+++ b/src/server/axum/api.rs
@@ -12,7 +12,7 @@ use axum_range::{KnownSize, Ranged};
 use tower_http::{compression::CompressionLayer, CompressionLevel};
 
 use crate::{
-	app::{ddns, index, peaks, playlist, scanner, settings, thumbnail, user, vfs, App},
+	app::{config, index, peaks, playlist, scanner, thumbnail, App},
 	server::{
 		dto, error::APIError, APIMajorVersion, API_ARRAY_SEPARATOR, API_MAJOR_VERSION,
 		API_MINOR_VERSION,
@@ -32,8 +32,6 @@ pub fn router() -> Router<App> {
 		.route("/settings", put(put_settings))
 		.route("/mount_dirs", get(get_mount_dirs))
 		.route("/mount_dirs", put(put_mount_dirs))
-		.route("/ddns", get(get_ddns))
-		.route("/ddns", put(put_ddns))
 		.route("/trigger_index", post(post_trigger_index))
 		// User management
 		.route("/user", post(post_user))
@@ -88,11 +86,11 @@ async fn get_version() -> Json<dto::Version> {
 }
 
 async fn get_initial_setup(
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 ) -> Result<Json<dto::InitialSetup>, APIError> {
 	let initial_setup = {
-		let users = user_manager.list().await?;
-		let has_any_admin = users.iter().any(|u| u.is_admin());
+		let users = config_manager.get_users().await;
+		let has_any_admin = users.iter().any(|u| u.admin == Some(true));
 		dto::InitialSetup {
 			has_any_users: has_any_admin,
 		}
@@ -101,16 +99,23 @@ async fn get_initial_setup(
 }
 
 async fn get_settings(
-	State(settings_manager): State<settings::Manager>,
 	_admin_rights: AdminRights,
+	State(config_manager): State<config::Manager>,
 ) -> Result<Json<dto::Settings>, APIError> {
-	let settings = settings_manager.read().await?;
-	Ok(Json(settings.into()))
+	let settings = dto::Settings {
+		album_art_pattern: config_manager.get_index_album_art_pattern().await,
+		reindex_every_n_seconds: config_manager.get_index_sleep_duration().await.as_secs(),
+		ddns_update_url: config_manager
+			.get_ddns_update_url()
+			.await
+			.unwrap_or_default(),
+	};
+	Ok(Json(settings))
 }
 
 async fn put_settings(
 	_admin_rights: AdminRights,
-	State(settings_manager): State<settings::Manager>,
+	State(config_manager): State<config::Manager>,
 	Json(new_settings): Json<dto::NewSettings>,
 ) -> Result<(), APIError> {
 	settings_manager
@@ -121,43 +126,26 @@ async fn put_settings(
 
 async fn get_mount_dirs(
 	_admin_rights: AdminRights,
-	State(vfs_manager): State<vfs::Manager>,
+	State(config_manager): State<config::Manager>,
 ) -> Result<Json<Vec<dto::MountDir>>, APIError> {
-	let mount_dirs = vfs_manager.mount_dirs().await?;
+	let mount_dirs = config_manager.get_mounts().await;
 	let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect();
 	Ok(Json(mount_dirs))
 }
 
 async fn put_mount_dirs(
 	_admin_rights: AdminRights,
-	State(vfs_manager): State<vfs::Manager>,
+	State(config_manager): State<config::Manager>,
 	new_mount_dirs: Json<Vec<dto::MountDir>>,
 ) -> Result<(), APIError> {
-	let new_mount_dirs: Vec<vfs::MountDir> =
+	let new_mount_dirs: Vec<config::MountDir> =
 		new_mount_dirs.iter().cloned().map(|m| m.into()).collect();
-	vfs_manager.set_mount_dirs(&new_mount_dirs).await?;
-	Ok(())
-}
-
-async fn get_ddns(
-	_admin_rights: AdminRights,
-	State(ddns_manager): State<ddns::Manager>,
-) -> Result<Json<dto::DDNSConfig>, APIError> {
-	let ddns_config = ddns_manager.config().await?;
-	Ok(Json(ddns_config.into()))
-}
-
-async fn put_ddns(
-	_admin_rights: AdminRights,
-	State(ddns_manager): State<ddns::Manager>,
-	Json(new_ddns_config): Json<dto::DDNSConfig>,
-) -> Result<(), APIError> {
-	ddns_manager.set_config(&new_ddns_config.into()).await?;
+	config_manager.set_mounts(new_mount_dirs).await;
 	Ok(())
 }
 
 async fn post_auth(
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 	credentials: Json<dto::Credentials>,
 ) -> Result<Json<dto::Authorization>, APIError> {
 	let username = credentials.username.clone();
@@ -178,16 +166,16 @@ async fn post_auth(
 
 async fn get_users(
 	_admin_rights: AdminRights,
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 ) -> Result<Json<Vec<dto::User>>, APIError> {
-	let users = user_manager.list().await?;
+	let users = config_manager.get_users().await;
 	let users = users.into_iter().map(|u| u.into()).collect();
 	Ok(Json(users))
 }
 
 async fn post_user(
 	_admin_rights: AdminRights,
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 	Json(new_user): Json<dto::NewUser>,
 ) -> Result<(), APIError> {
 	user_manager.create(&new_user.into()).await?;
@@ -196,7 +184,7 @@ async fn post_user(
 
 async fn put_user(
 	admin_rights: AdminRights,
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 	Path(name): Path<String>,
 	user_update: Json<dto::UserUpdate>,
 ) -> Result<(), APIError> {
@@ -219,7 +207,7 @@ async fn put_user(
 
 async fn delete_user(
 	admin_rights: AdminRights,
-	State(user_manager): State<user::Manager>,
+	State(config_manager): State<config::Manager>,
 	Path(name): Path<String>,
 ) -> Result<(), APIError> {
 	if let Some(auth) = &admin_rights.get_auth() {
@@ -227,22 +215,26 @@ async fn delete_user(
 			return Err(APIError::DeletingOwnAccount);
 		}
 	}
-	user_manager.delete(&name).await?;
+	config_manager.delete_user(&name).await;
 	Ok(())
 }
 
 async fn get_preferences(
 	auth: Auth,
-	State(user_manager): State<user::Manager>,
-) -> Result<Json<user::Preferences>, APIError> {
-	let preferences = user_manager.read_preferences(auth.get_username()).await?;
+	State(config_manager): State<config::Manager>,
+) -> Result<Json<dto::Preferences>, APIError> {
+	let user = config_manager.get_user(auth.get_username()).await?;
+	let preferences = dto::Preferences {
+		web_theme_base: user.web_theme_base,
+		web_theme_accent: user.web_theme_accent,
+	};
 	Ok(Json(preferences))
 }
 
 async fn put_preferences(
 	auth: Auth,
-	State(user_manager): State<user::Manager>,
-	Json(preferences): Json<user::Preferences>,
+	State(config_manager): State<config::Manager>,
+	Json(preferences): Json<dto::NewPreferences>,
 ) -> Result<(), APIError> {
 	user_manager
 		.write_preferences(auth.get_username(), &preferences)
@@ -450,12 +442,11 @@ async fn get_songs(
 
 async fn get_peaks(
 	_auth: Auth,
-	State(vfs_manager): State<vfs::Manager>,
+	State(config_manager): State<config::Manager>,
 	State(peaks_manager): State<peaks::Manager>,
 	Path(path): Path<PathBuf>,
 ) -> Result<dto::Peaks, APIError> {
-	let vfs = vfs_manager.get_vfs().await?;
-	let audio_path = vfs.virtual_to_real(&path)?;
+	let audio_path = config_manager.resolve_virtual_path(&path).await?;
 	let peaks = peaks_manager.get_peaks(&audio_path).await?;
 	Ok(peaks.interleaved)
 }
@@ -662,12 +653,11 @@ async fn delete_playlist(
 
 async fn get_audio(
 	_auth: Auth,
-	State(vfs_manager): State<vfs::Manager>,
+	State(config_manager): State<config::Manager>,
 	Path(path): Path<PathBuf>,
 	range: Option<TypedHeader<Range>>,
 ) -> Result<impl IntoResponse, APIError> {
-	let vfs = vfs_manager.get_vfs().await?;
-	let audio_path = vfs.virtual_to_real(&path)?;
+	let audio_path = config_manager.resolve_virtual_path(&path).await?;
 
 	let Ok(file) = tokio::fs::File::open(audio_path).await else {
 		return Err(APIError::AudioFileIOError);
@@ -683,15 +673,14 @@ async fn get_audio(
 
 async fn get_thumbnail(
 	_auth: Auth,
-	State(vfs_manager): State<vfs::Manager>,
+	State(config_manager): State<config::Manager>,
 	State(thumbnails_manager): State<thumbnail::Manager>,
 	Path(path): Path<PathBuf>,
 	Query(options_input): Query<dto::ThumbnailOptions>,
 	range: Option<TypedHeader<Range>>,
 ) -> Result<impl IntoResponse, APIError> {
 	let options = thumbnail::Options::from(options_input);
-	let vfs = vfs_manager.get_vfs().await?;
-	let image_path = vfs.virtual_to_real(&path)?;
+	let image_path = config_manager.resolve_virtual_path(&path).await?;
 
 	let thumbnail_path = thumbnails_manager
 		.get_thumbnail(&image_path, &options)
diff --git a/src/server/axum/auth.rs b/src/server/axum/auth.rs
index de816db..eddb5ff 100644
--- a/src/server/axum/auth.rs
+++ b/src/server/axum/auth.rs
@@ -6,7 +6,7 @@ use headers::authorization::{Bearer, Credentials};
 use http::request::Parts;
 
 use crate::{
-	app::user,
+	app::config,
 	server::{dto, error::APIError},
 };
 
@@ -24,13 +24,13 @@ impl Auth {
 #[async_trait]
 impl<S> FromRequestParts<S> for Auth
 where
-	user::Manager: FromRef<S>,
+	config::Manager: FromRef<S>,
 	S: Send + Sync,
 {
 	type Rejection = APIError;
 
 	async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> {
-		let user_manager = user::Manager::from_ref(app);
+		let config_manager = config::Manager::from_ref(app);
 
 		let header_token = parts
 			.headers
@@ -73,13 +73,13 @@ impl AdminRights {
 #[async_trait]
 impl<S> FromRequestParts<S> for AdminRights
 where
-	user::Manager: FromRef<S>,
+	config::Manager: FromRef<S>,
 	S: Send + Sync,
 {
 	type Rejection = APIError;
 
 	async fn from_request_parts(parts: &mut Parts, app: &S) -> Result<Self, Self::Rejection> {
-		let user_manager = user::Manager::from_ref(app);
+		let config_manager = config::Manager::from_ref(app);
 
 		let user_count = user_manager.count().await?;
 		if user_count == 0 {
diff --git a/src/server/axum/error.rs b/src/server/axum/error.rs
index c19de67..4c7a5ba 100644
--- a/src/server/axum/error.rs
+++ b/src/server/axum/error.rs
@@ -19,7 +19,6 @@ impl IntoResponse for APIError {
 				StatusCode::from_u16(s).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
 			}
 			APIError::NativeDatabase(_) => StatusCode::INTERNAL_SERVER_ERROR,
-			APIError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
 			APIError::DeletingOwnAccount => StatusCode::CONFLICT,
 			APIError::DirectoryNotFound(_) => StatusCode::NOT_FOUND,
 			APIError::ArtistNotFound => StatusCode::NOT_FOUND,
diff --git a/src/server/axum/test.rs b/src/server/axum/test.rs
index dba2266..01eb99c 100644
--- a/src/server/axum/test.rs
+++ b/src/server/axum/test.rs
@@ -23,7 +23,7 @@ impl TestService for AxumTestService {
 
 		let paths = Paths {
 			cache_dir_path: ["test-output", test_name].iter().collect(),
-			config_file_path: None,
+			config_file_path: output_dir.join("polaris.toml"),
 			data_dir_path: ["test-output", test_name].iter().collect(),
 			db_file_path: output_dir.join("db.sqlite"),
 			#[cfg(unix)]
diff --git a/src/server/dto/v8.rs b/src/server/dto/v8.rs
index ad43fde..fe8760f 100644
--- a/src/server/dto/v8.rs
+++ b/src/server/dto/v8.rs
@@ -1,6 +1,6 @@
 use serde::{Deserialize, Serialize};
 
-use crate::app::{config, ddns, index, peaks, playlist, settings, thumbnail, user, vfs};
+use crate::app::{config, index, peaks, playlist, thumbnail, user};
 use std::{collections::HashMap, convert::From, path::PathBuf};
 
 #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
@@ -111,11 +111,11 @@ pub struct User {
 	pub is_admin: bool,
 }
 
-impl From<user::User> for User {
-	fn from(u: user::User) -> Self {
+impl From<config::User> for User {
+	fn from(u: config::User) -> Self {
 		Self {
 			name: u.name,
-			is_admin: u.admin != 0,
+			is_admin: u.admin == Some(true),
 		}
 	}
 }
@@ -143,31 +143,16 @@ pub struct UserUpdate {
 	pub new_is_admin: Option<bool>,
 }
 
-#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
-pub struct DDNSConfig {
-	pub host: String,
-	pub username: String,
-	pub password: String,
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Preferences {
+	pub web_theme_base: Option<String>,
+	pub web_theme_accent: Option<String>,
 }
 
-impl From<DDNSConfig> for ddns::Config {
-	fn from(c: DDNSConfig) -> Self {
-		Self {
-			ddns_host: c.host,
-			ddns_username: c.username,
-			ddns_password: c.password,
-		}
-	}
-}
-
-impl From<ddns::Config> for DDNSConfig {
-	fn from(c: ddns::Config) -> Self {
-		Self {
-			host: c.ddns_host,
-			username: c.ddns_username,
-			password: c.ddns_password,
-		}
-	}
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub struct NewPreferences {
+	pub web_theme_base: Option<String>,
+	pub web_theme_accent: Option<String>,
 }
 
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
@@ -176,7 +161,7 @@ pub struct MountDir {
 	pub name: String,
 }
 
-impl From<MountDir> for vfs::MountDir {
+impl From<MountDir> for config::MountDir {
 	fn from(m: MountDir) -> Self {
 		Self {
 			name: m.name,
@@ -185,8 +170,8 @@ impl From<MountDir> for vfs::MountDir {
 	}
 }
 
-impl From<vfs::MountDir> for MountDir {
-	fn from(m: vfs::MountDir) -> Self {
+impl From<config::MountDir> for MountDir {
+	fn from(m: config::MountDir) -> Self {
 		Self {
 			name: m.name,
 			source: m.source,
@@ -194,55 +179,18 @@ impl From<vfs::MountDir> for MountDir {
 	}
 }
 
-#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Config {
-	pub settings: Option<NewSettings>,
-	pub users: Option<Vec<NewUser>>,
-	pub mount_dirs: Option<Vec<MountDir>>,
-	pub ydns: Option<DDNSConfig>,
-}
-
-impl From<Config> for config::Config {
-	fn from(s: Config) -> Self {
-		Self {
-			settings: s.settings.map(|s| s.into()),
-			mount_dirs: s
-				.mount_dirs
-				.map(|v| v.into_iter().map(|m| m.into()).collect()),
-			users: s.users.map(|v| v.into_iter().map(|u| u.into()).collect()),
-			ydns: s.ydns.map(|c| c.into()),
-		}
-	}
-}
-
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct NewSettings {
 	pub album_art_pattern: Option<String>,
 	pub reindex_every_n_seconds: Option<i64>,
-}
-
-impl From<NewSettings> for settings::NewSettings {
-	fn from(s: NewSettings) -> Self {
-		Self {
-			album_art_pattern: s.album_art_pattern,
-			reindex_every_n_seconds: s.reindex_every_n_seconds,
-		}
-	}
+	pub ddns_update_url: Option<String>,
 }
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Settings {
 	pub album_art_pattern: String,
-	pub reindex_every_n_seconds: i64,
-}
-
-impl From<settings::Settings> for Settings {
-	fn from(s: settings::Settings) -> Self {
-		Self {
-			album_art_pattern: s.index_album_art_pattern,
-			reindex_every_n_seconds: s.index_sleep_duration_seconds,
-		}
-	}
+	pub reindex_every_n_seconds: u64,
+	pub ddns_update_url: String,
 }
 
 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -513,5 +461,3 @@ pub struct GetRecentAlbumsParameters {
 	pub offset: Option<usize>,
 	pub count: Option<usize>,
 }
-
-// TODO: Preferences should have dto types
diff --git a/src/server/error.rs b/src/server/error.rs
index 04c8de9..901b150 100644
--- a/src/server/error.rs
+++ b/src/server/error.rs
@@ -21,8 +21,6 @@ pub enum APIError {
 	AuthenticationRequired,
 	#[error("Could not encode Branca token")]
 	BrancaTokenEncoding,
-	#[error("Database error:\n\n{0}")]
-	Database(sqlx::Error),
 	#[error("Native Database error:\n\n{0}")]
 	NativeDatabase(native_db::db_type::Error),
 	#[error("Directory not found: {0}")]
@@ -110,11 +108,6 @@ impl From<app::Error> for APIError {
 			app::Error::NativeDatabaseCreationError(_) => APIError::Internal,
 			app::Error::NativeDatabase(e) => APIError::NativeDatabase(e),
 
-			app::Error::Database(e) => APIError::Database(e),
-			app::Error::ConnectionPoolBuild => APIError::Internal,
-			app::Error::ConnectionPool => APIError::Internal,
-			app::Error::Migration(_) => APIError::Internal,
-
 			app::Error::UpdateQueryFailed(s) => APIError::DdnsUpdateQueryFailed(s),
 			app::Error::UpdateQueryTransport => APIError::DdnsUpdateQueryFailed(0),
 
diff --git a/src/server/test/protocol.rs b/src/server/test/protocol.rs
index d08d2ef..21faa4c 100644
--- a/src/server/test/protocol.rs
+++ b/src/server/test/protocol.rs
@@ -3,7 +3,7 @@ use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
 use std::path::Path;
 
 use crate::server::dto;
-use crate::{app::user, server::dto::ThumbnailSize};
+use crate::server::dto::ThumbnailSize;
 
 pub trait ProtocolVersion {
 	fn header_value() -> i32;
@@ -92,22 +92,6 @@ pub fn put_settings(settings: dto::NewSettings) -> Request<dto::NewSettings> {
 		.unwrap()
 }
 
-pub fn get_ddns_config() -> Request<()> {
-	Request::builder()
-		.method(Method::GET)
-		.uri("/api/ddns")
-		.body(())
-		.unwrap()
-}
-
-pub fn put_ddns_config(ddns_config: dto::DDNSConfig) -> Request<dto::DDNSConfig> {
-	Request::builder()
-		.method(Method::PUT)
-		.uri("/api/ddns")
-		.body(ddns_config)
-		.unwrap()
-}
-
 pub fn list_users() -> Request<()> {
 	Request::builder()
 		.method(Method::GET)
@@ -148,7 +132,7 @@ pub fn get_preferences() -> Request<()> {
 		.unwrap()
 }
 
-pub fn put_preferences(preferences: user::Preferences) -> Request<user::Preferences> {
+pub fn put_preferences(preferences: dto::NewPreferences) -> Request<dto::NewPreferences> {
 	Request::builder()
 		.method(Method::PUT)
 		.uri("/api/preferences")
diff --git a/src/server/test/user.rs b/src/server/test/user.rs
index 1d49a9d..c05cd84 100644
--- a/src/server/test/user.rs
+++ b/src/server/test/user.rs
@@ -1,7 +1,6 @@
 use http::StatusCode;
 use std::default::Default;
 
-use crate::app::user;
 use crate::server::dto;
 use crate::server::test::{constants::*, protocol, ServiceType, TestService};
 use crate::test_name;
@@ -156,14 +155,14 @@ async fn get_preferences_golden_path() {
 	service.login().await;
 
 	let request = protocol::get_preferences();
-	let response = service.fetch_json::<_, user::Preferences>(&request).await;
+	let response = service.fetch_json::<_, dto::Preferences>(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }
 
 #[tokio::test]
 async fn put_preferences_requires_auth() {
 	let mut service = ServiceType::new(&test_name!()).await;
-	let request = protocol::put_preferences(user::Preferences::default());
+	let request = protocol::put_preferences(dto::NewPreferences::default());
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
 }
@@ -174,7 +173,7 @@ async fn put_preferences_golden_path() {
 	service.complete_initial_setup().await;
 	service.login().await;
 
-	let request = protocol::put_preferences(user::Preferences::default());
+	let request = protocol::put_preferences(dto::NewPreferences::default());
 	let response = service.fetch(&request).await;
 	assert_eq!(response.status(), StatusCode::OK);
 }