diff --git a/.idea/code.iml b/.idea/code.iml
index 3861923c60..a1d7c854b4 100644
--- a/.idea/code.iml
+++ b/.idea/code.iml
@@ -28,6 +28,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 0772a3a9c1..89ee75a3b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2223,6 +2223,16 @@ dependencies = [
"static_assertions",
]
+[[package]]
+name = "component-derive"
+version = "0.0.0"
+dependencies = [
+ "darling 0.23.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
[[package]]
name = "compression-codecs"
version = "0.4.31"
@@ -5480,6 +5490,7 @@ dependencies = [
"clickhouse",
"color-eyre",
"color-thief",
+ "component-derive",
"const_format",
"dashmap",
"derive_more 2.1.1",
@@ -5557,16 +5568,6 @@ dependencies = [
"zxcvbn",
]
-[[package]]
-name = "labrinth-derive"
-version = "0.0.0"
-dependencies = [
- "darling 0.23.0",
- "proc-macro2",
- "quote",
- "syn 2.0.106",
-]
-
[[package]]
name = "language-tags"
version = "0.3.2"
diff --git a/Cargo.toml b/Cargo.toml
index ab510f52d7..783f73631a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,8 +7,8 @@ members = [
"apps/labrinth",
"packages/app-lib",
"packages/ariadne",
+ "packages/component-derive",
"packages/daedalus",
- "packages/labrinth-derive",
"packages/modrinth-content-management",
"packages/modrinth-log",
"packages/modrinth-maxmind",
@@ -68,6 +68,7 @@ clap = "4.5.48"
clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
+component-derive = { path = "packages/component-derive" }
const_format = "0.2.34"
core-foundation = "0.10.1"
core-graphics = "0.24.0"
diff --git a/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json b/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json
new file mode 100644
index 0000000000..7aa05f7bab
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT preferences AS \"preferences: Json\"\n FROM user_preferences\n WHERE user_id = $1\n FOR UPDATE\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "preferences: Json",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "829fa6bb9dd88f401abc4b5164d69f43909750257d6b4f86cfecc89568b86cb9"
+}
diff --git a/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json b/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json
new file mode 100644
index 0000000000..431861125a
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT preferences AS \"preferences: Json\"\n FROM user_preferences\n WHERE user_id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "preferences: Json",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "9c97cb31c02777c10c329d17504267e8328df9846d35c4df149752098a750caa"
+}
diff --git a/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json b/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json
new file mode 100644
index 0000000000..d27a0354eb
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO user_preferences (user_id, preferences)\n VALUES ($1, $2)\n ON CONFLICT (user_id) DO UPDATE\n SET preferences = EXCLUDED.preferences\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Jsonb"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "b301f61d9e57ba351cc12f47066823cd02c215c4b219859724928ebdb99e493f"
+}
diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml
index fbfe31b0bf..6c3ab65446 100644
--- a/apps/labrinth/Cargo.toml
+++ b/apps/labrinth/Cargo.toml
@@ -40,6 +40,7 @@ clap = { workspace = true, features = ["derive"] }
clickhouse = { workspace = true, features = ["time", "uuid"] }
color-eyre = { workspace = true }
color-thief = { workspace = true }
+component-derive = { workspace = true }
const_format = { workspace = true }
dashmap = { workspace = true }
derive_more = { workspace = true, features = ["deref", "deref_mut"] }
diff --git a/apps/labrinth/migrations/20260806120000_user-preferences.sql b/apps/labrinth/migrations/20260806120000_user-preferences.sql
new file mode 100644
index 0000000000..ed9409d52b
--- /dev/null
+++ b/apps/labrinth/migrations/20260806120000_user-preferences.sql
@@ -0,0 +1,4 @@
+CREATE TABLE user_preferences (
+ user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
+ preferences JSONB NOT NULL
+);
diff --git a/apps/labrinth/src/database/models/mod.rs b/apps/labrinth/src/database/models/mod.rs
index 896791e7d3..e647428669 100644
--- a/apps/labrinth/src/database/models/mod.rs
+++ b/apps/labrinth/src/database/models/mod.rs
@@ -38,6 +38,7 @@ pub mod team_item;
pub mod thread_item;
pub mod user_item;
pub mod user_limits;
+pub mod user_preferences_item;
pub mod user_subscription_item;
pub mod users_compliance;
pub mod users_notifications_preferences_item;
diff --git a/apps/labrinth/src/database/models/user_preferences_item.rs b/apps/labrinth/src/database/models/user_preferences_item.rs
new file mode 100644
index 0000000000..853a6397e2
--- /dev/null
+++ b/apps/labrinth/src/database/models/user_preferences_item.rs
@@ -0,0 +1,75 @@
+use crate::database::Executor;
+use crate::database::models::DBUserId;
+use crate::models::v3::preferences::PartialUserPreferences;
+use sqlx::types::Json;
+
+pub struct DBUserPreferences;
+
+impl DBUserPreferences {
+ pub async fn get<'a, E>(
+ user_id: DBUserId,
+ exec: E,
+ ) -> Result