diff --git a/.gitattributes b/.gitattributes
index d321e2a910..2e8a45ca5c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,6 +6,9 @@ mobile/openapi/**/*.dart linguist-generated=true
 mobile/lib/**/*.g.dart -diff -merge
 mobile/lib/**/*.g.dart linguist-generated=true
 
+mobile/lib/**/*.drift.dart -diff -merge
+mobile/lib/**/*.drift.dart linguist-generated=true
+
 open-api/typescript-sdk/fetch-client.ts -diff -merge
 open-api/typescript-sdk/fetch-client.ts linguist-generated=true
 
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 49dbf3944c..49692809bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -39,6 +39,7 @@
   ],
   "explorer.fileNesting.enabled": true,
   "explorer.fileNesting.patterns": {
-    "*.ts": "${capture}.spec.ts,${capture}.mock.ts"
+    "*.ts": "${capture}.spec.ts,${capture}.mock.ts",
+    "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
   }
 }
\ No newline at end of file
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index 085449756d..07c6f65b71 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -36,6 +36,8 @@ analyzer:
   exclude:
     - openapi/**
     - lib/generated_plugin_registrant.dart
+    - lib/**/*.g.dart
+    - lib/**/*.drift.dart
 
   plugins:
     - custom_lint
diff --git a/mobile/build.yaml b/mobile/build.yaml
new file mode 100644
index 0000000000..d5de77a377
--- /dev/null
+++ b/mobile/build.yaml
@@ -0,0 +1,24 @@
+targets:
+   $default:
+     builders:
+       #drift @DriftDatabase()
+       drift_dev:
+         # Disable default builder to use modular builder instead
+         enabled: false
+       drift_dev:analyzer:
+         enabled: true
+         options: &drift_options
+           store_date_time_values_as_text: true
+           named_parameters: true
+           write_from_json_string_constructor: false
+           data_class_to_companions: false
+           # Required for make-migrations
+           databases:
+             main: lib/infrastructure/repositories/db.repository.dart
+         generate_for: &drift_generate_for
+           - lib/infrastructure/entities/*.dart
+           - lib/infrastructure/repositories/db.repository.dart
+       drift_dev:modular:
+         enabled: true
+         options: *drift_options
+         generate_for: *drift_generate_for
\ No newline at end of file
diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json
new file mode 100644
index 0000000000..1870ef477f
--- /dev/null
+++ b/mobile/drift_schemas/main/drift_schema_v1.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]}
\ No newline at end of file
diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart
index ad241a8c48..abf2e5620b 100644
--- a/mobile/lib/domain/models/user.model.dart
+++ b/mobile/lib/domain/models/user.model.dart
@@ -1,32 +1,4 @@
-import 'dart:ui';
-
-enum AvatarColor {
-  // do not change this order or reuse indices for other purposes, adding is OK
-  primary,
-  pink,
-  red,
-  yellow,
-  blue,
-  green,
-  purple,
-  orange,
-  gray,
-  amber;
-
-  Color toColor({bool isDarkTheme = false}) => switch (this) {
-        AvatarColor.primary =>
-          isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
-        AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
-        AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
-        AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
-        AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
-        AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
-        AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
-        AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
-        AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
-        AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
-      };
-}
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
 
 // TODO: Rename to User once Isar is removed
 class UserDto {
diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart
new file mode 100644
index 0000000000..1586384422
--- /dev/null
+++ b/mobile/lib/domain/models/user_metadata.model.dart
@@ -0,0 +1,105 @@
+import 'dart:ui';
+
+enum AvatarColor {
+  // do not change this order or reuse indices for other purposes, adding is OK
+  primary("primary"),
+  pink("pink"),
+  red("red"),
+  yellow("yellow"),
+  blue("blue"),
+  green("green"),
+  purple("purple"),
+  orange("orange"),
+  gray("gray"),
+  amber("amber");
+
+  final String value;
+  const AvatarColor(this.value);
+
+  Color toColor({bool isDarkTheme = false}) => switch (this) {
+        AvatarColor.primary =>
+          isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
+        AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
+        AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
+        AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
+        AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
+        AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
+        AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
+        AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
+        AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
+        AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
+      };
+}
+
+class UserPreferences {
+  final bool foldersEnabled;
+  final bool memoriesEnabled;
+  final bool peopleEnabled;
+  final bool ratingsEnabled;
+  final bool sharedLinksEnabled;
+  final bool tagsEnabled;
+  final AvatarColor userAvatarColor;
+  final bool showSupportBadge;
+
+  const UserPreferences({
+    this.foldersEnabled = false,
+    this.memoriesEnabled = true,
+    this.peopleEnabled = true,
+    this.ratingsEnabled = false,
+    this.sharedLinksEnabled = true,
+    this.tagsEnabled = false,
+    this.userAvatarColor = AvatarColor.primary,
+    this.showSupportBadge = true,
+  });
+
+  UserPreferences copyWith({
+    bool? foldersEnabled,
+    bool? memoriesEnabled,
+    bool? peopleEnabled,
+    bool? ratingsEnabled,
+    bool? sharedLinksEnabled,
+    bool? tagsEnabled,
+    AvatarColor? userAvatarColor,
+    bool? showSupportBadge,
+  }) {
+    return UserPreferences(
+      foldersEnabled: foldersEnabled ?? this.foldersEnabled,
+      memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled,
+      peopleEnabled: peopleEnabled ?? this.peopleEnabled,
+      ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled,
+      sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled,
+      tagsEnabled: tagsEnabled ?? this.tagsEnabled,
+      userAvatarColor: userAvatarColor ?? this.userAvatarColor,
+      showSupportBadge: showSupportBadge ?? this.showSupportBadge,
+    );
+  }
+
+  Map<String, Object?> toMap() {
+    final preferences = <String, Object?>{};
+    preferences["folders-Enabled"] = foldersEnabled;
+    preferences["memories-Enabled"] = memoriesEnabled;
+    preferences["people-Enabled"] = peopleEnabled;
+    preferences["ratings-Enabled"] = ratingsEnabled;
+    preferences["sharedLinks-Enabled"] = sharedLinksEnabled;
+    preferences["tags-Enabled"] = tagsEnabled;
+    preferences["avatar-Color"] = userAvatarColor.value;
+    preferences["purchase-ShowSupportBadge"] = showSupportBadge;
+    return preferences;
+  }
+
+  factory UserPreferences.fromMap(Map<String, Object?> map) {
+    return UserPreferences(
+      foldersEnabled: map["folders-Enabled"] as bool? ?? false,
+      memoriesEnabled: map["memories-Enabled"] as bool? ?? true,
+      peopleEnabled: map["people-Enabled"] as bool? ?? true,
+      ratingsEnabled: map["ratings-Enabled"] as bool? ?? false,
+      sharedLinksEnabled: map["sharedLinks-Enabled"] as bool? ?? true,
+      tagsEnabled: map["tags-Enabled"] as bool? ?? false,
+      userAvatarColor: AvatarColor.values.firstWhere(
+        (e) => e.value == map["avatar-Color"] as String?,
+        orElse: () => AvatarColor.primary,
+      ),
+      showSupportBadge: map["purchase-ShowSupportBadge"] as bool? ?? true,
+    );
+  }
+}
diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart
new file mode 100644
index 0000000000..b7925a8eea
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/partner.entity.dart
@@ -0,0 +1,18 @@
+import 'package:drift/drift.dart';
+import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
+import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
+
+class PartnerEntity extends Table with DriftDefaultsMixin {
+  const PartnerEntity();
+
+  BlobColumn get sharedById =>
+      blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
+
+  BlobColumn get sharedWithId =>
+      blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
+
+  BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();
+
+  @override
+  Set<Column> get primaryKey => {sharedById, sharedWithId};
+}
diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart
new file mode 100644
index 0000000000..974a9e3c30
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart
@@ -0,0 +1,610 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
+    as i1;
+import 'dart:typed_data' as i2;
+import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'
+    as i3;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
+import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
+    as i5;
+import 'package:drift/internal/modular.dart' as i6;
+
+typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion
+    Function({
+  required i2.Uint8List sharedById,
+  required i2.Uint8List sharedWithId,
+  i0.Value<bool> inTimeline,
+});
+typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion
+    Function({
+  i0.Value<i2.Uint8List> sharedById,
+  i0.Value<i2.Uint8List> sharedWithId,
+  i0.Value<bool> inTimeline,
+});
+
+final class $$PartnerEntityTableReferences extends i0.BaseReferences<
+    i0.GeneratedDatabase, i1.$PartnerEntityTable, i1.PartnerEntityData> {
+  $$PartnerEntityTableReferences(
+      super.$_db, super.$_table, super.$_typedResult);
+
+  static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
+      i6.ReadDatabaseContainer(db)
+          .resultSet<i5.$UserEntityTable>('user_entity')
+          .createAlias(i0.$_aliasNameGenerator(
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i1.$PartnerEntityTable>('partner_entity')
+                  .sharedById,
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i5.$UserEntityTable>('user_entity')
+                  .id));
+
+  i5.$$UserEntityTableProcessedTableManager get sharedById {
+    final $_column = $_itemColumn<i2.Uint8List>('shared_by_id')!;
+
+    final manager = i5
+        .$$UserEntityTableTableManager(
+            $_db,
+            i6.ReadDatabaseContainer($_db)
+                .resultSet<i5.$UserEntityTable>('user_entity'))
+        .filter((f) => f.id.sqlEquals($_column));
+    final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db));
+    if (item == null) return manager;
+    return i0.ProcessedTableManager(
+        manager.$state.copyWith(prefetchedData: [item]));
+  }
+
+  static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
+      i6.ReadDatabaseContainer(db)
+          .resultSet<i5.$UserEntityTable>('user_entity')
+          .createAlias(i0.$_aliasNameGenerator(
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i1.$PartnerEntityTable>('partner_entity')
+                  .sharedWithId,
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i5.$UserEntityTable>('user_entity')
+                  .id));
+
+  i5.$$UserEntityTableProcessedTableManager get sharedWithId {
+    final $_column = $_itemColumn<i2.Uint8List>('shared_with_id')!;
+
+    final manager = i5
+        .$$UserEntityTableTableManager(
+            $_db,
+            i6.ReadDatabaseContainer($_db)
+                .resultSet<i5.$UserEntityTable>('user_entity'))
+        .filter((f) => f.id.sqlEquals($_column));
+    final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db));
+    if (item == null) return manager;
+    return i0.ProcessedTableManager(
+        manager.$state.copyWith(prefetchedData: [item]));
+  }
+}
+
+class $$PartnerEntityTableFilterComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
+  $$PartnerEntityTableFilterComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnFilters<bool> get inTimeline => $composableBuilder(
+      column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column));
+
+  i5.$$UserEntityTableFilterComposer get sharedById {
+    final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedById,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableFilterComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+
+  i5.$$UserEntityTableFilterComposer get sharedWithId {
+    final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedWithId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableFilterComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$PartnerEntityTableOrderingComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
+  $$PartnerEntityTableOrderingComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnOrderings<bool> get inTimeline => $composableBuilder(
+      column: $table.inTimeline,
+      builder: (column) => i0.ColumnOrderings(column));
+
+  i5.$$UserEntityTableOrderingComposer get sharedById {
+    final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedById,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableOrderingComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+
+  i5.$$UserEntityTableOrderingComposer get sharedWithId {
+    final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedWithId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableOrderingComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$PartnerEntityTableAnnotationComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$PartnerEntityTable> {
+  $$PartnerEntityTableAnnotationComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.GeneratedColumn<bool> get inTimeline => $composableBuilder(
+      column: $table.inTimeline, builder: (column) => column);
+
+  i5.$$UserEntityTableAnnotationComposer get sharedById {
+    final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedById,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableAnnotationComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+
+  i5.$$UserEntityTableAnnotationComposer get sharedWithId {
+    final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.sharedWithId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableAnnotationComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$PartnerEntityTableTableManager extends i0.RootTableManager<
+    i0.GeneratedDatabase,
+    i1.$PartnerEntityTable,
+    i1.PartnerEntityData,
+    i1.$$PartnerEntityTableFilterComposer,
+    i1.$$PartnerEntityTableOrderingComposer,
+    i1.$$PartnerEntityTableAnnotationComposer,
+    $$PartnerEntityTableCreateCompanionBuilder,
+    $$PartnerEntityTableUpdateCompanionBuilder,
+    (i1.PartnerEntityData, i1.$$PartnerEntityTableReferences),
+    i1.PartnerEntityData,
+    i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})> {
+  $$PartnerEntityTableTableManager(
+      i0.GeneratedDatabase db, i1.$PartnerEntityTable table)
+      : super(i0.TableManagerState(
+          db: db,
+          table: table,
+          createFilteringComposer: () =>
+              i1.$$PartnerEntityTableFilterComposer($db: db, $table: table),
+          createOrderingComposer: () =>
+              i1.$$PartnerEntityTableOrderingComposer($db: db, $table: table),
+          createComputedFieldComposer: () =>
+              i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table),
+          updateCompanionCallback: ({
+            i0.Value<i2.Uint8List> sharedById = const i0.Value.absent(),
+            i0.Value<i2.Uint8List> sharedWithId = const i0.Value.absent(),
+            i0.Value<bool> inTimeline = const i0.Value.absent(),
+          }) =>
+              i1.PartnerEntityCompanion(
+            sharedById: sharedById,
+            sharedWithId: sharedWithId,
+            inTimeline: inTimeline,
+          ),
+          createCompanionCallback: ({
+            required i2.Uint8List sharedById,
+            required i2.Uint8List sharedWithId,
+            i0.Value<bool> inTimeline = const i0.Value.absent(),
+          }) =>
+              i1.PartnerEntityCompanion.insert(
+            sharedById: sharedById,
+            sharedWithId: sharedWithId,
+            inTimeline: inTimeline,
+          ),
+          withReferenceMapper: (p0) => p0
+              .map((e) => (
+                    e.readTable(table),
+                    i1.$$PartnerEntityTableReferences(db, table, e)
+                  ))
+              .toList(),
+          prefetchHooksCallback: ({sharedById = false, sharedWithId = false}) {
+            return i0.PrefetchHooks(
+              db: db,
+              explicitlyWatchedTables: [],
+              addJoins: <
+                  T extends i0.TableManagerState<
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic>>(state) {
+                if (sharedById) {
+                  state = state.withJoin(
+                    currentTable: table,
+                    currentColumn: table.sharedById,
+                    referencedTable:
+                        i1.$$PartnerEntityTableReferences._sharedByIdTable(db),
+                    referencedColumn: i1.$$PartnerEntityTableReferences
+                        ._sharedByIdTable(db)
+                        .id,
+                  ) as T;
+                }
+                if (sharedWithId) {
+                  state = state.withJoin(
+                    currentTable: table,
+                    currentColumn: table.sharedWithId,
+                    referencedTable: i1.$$PartnerEntityTableReferences
+                        ._sharedWithIdTable(db),
+                    referencedColumn: i1.$$PartnerEntityTableReferences
+                        ._sharedWithIdTable(db)
+                        .id,
+                  ) as T;
+                }
+
+                return state;
+              },
+              getPrefetchedDataCallback: (items) async {
+                return [];
+              },
+            );
+          },
+        ));
+}
+
+typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager<
+    i0.GeneratedDatabase,
+    i1.$PartnerEntityTable,
+    i1.PartnerEntityData,
+    i1.$$PartnerEntityTableFilterComposer,
+    i1.$$PartnerEntityTableOrderingComposer,
+    i1.$$PartnerEntityTableAnnotationComposer,
+    $$PartnerEntityTableCreateCompanionBuilder,
+    $$PartnerEntityTableUpdateCompanionBuilder,
+    (i1.PartnerEntityData, i1.$$PartnerEntityTableReferences),
+    i1.PartnerEntityData,
+    i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>;
+
+class $PartnerEntityTable extends i3.PartnerEntity
+    with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> {
+  @override
+  final i0.GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  $PartnerEntityTable(this.attachedDatabase, [this._alias]);
+  static const i0.VerificationMeta _sharedByIdMeta =
+      const i0.VerificationMeta('sharedById');
+  @override
+  late final i0.GeneratedColumn<i2.Uint8List> sharedById =
+      i0.GeneratedColumn<i2.Uint8List>('shared_by_id', aliasedName, false,
+          type: i0.DriftSqlType.blob,
+          requiredDuringInsert: true,
+          defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
+              'REFERENCES user_entity (id) ON DELETE CASCADE'));
+  static const i0.VerificationMeta _sharedWithIdMeta =
+      const i0.VerificationMeta('sharedWithId');
+  @override
+  late final i0.GeneratedColumn<i2.Uint8List> sharedWithId =
+      i0.GeneratedColumn<i2.Uint8List>('shared_with_id', aliasedName, false,
+          type: i0.DriftSqlType.blob,
+          requiredDuringInsert: true,
+          defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
+              'REFERENCES user_entity (id) ON DELETE CASCADE'));
+  static const i0.VerificationMeta _inTimelineMeta =
+      const i0.VerificationMeta('inTimeline');
+  @override
+  late final i0.GeneratedColumn<bool> inTimeline = i0.GeneratedColumn<bool>(
+      'in_timeline', aliasedName, false,
+      type: i0.DriftSqlType.bool,
+      requiredDuringInsert: false,
+      defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
+          'CHECK ("in_timeline" IN (0, 1))'),
+      defaultValue: const i4.Constant(false));
+  @override
+  List<i0.GeneratedColumn> get $columns =>
+      [sharedById, sharedWithId, inTimeline];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'partner_entity';
+  @override
+  i0.VerificationContext validateIntegrity(
+      i0.Insertable<i1.PartnerEntityData> instance,
+      {bool isInserting = false}) {
+    final context = i0.VerificationContext();
+    final data = instance.toColumns(true);
+    if (data.containsKey('shared_by_id')) {
+      context.handle(
+          _sharedByIdMeta,
+          sharedById.isAcceptableOrUnknown(
+              data['shared_by_id']!, _sharedByIdMeta));
+    } else if (isInserting) {
+      context.missing(_sharedByIdMeta);
+    }
+    if (data.containsKey('shared_with_id')) {
+      context.handle(
+          _sharedWithIdMeta,
+          sharedWithId.isAcceptableOrUnknown(
+              data['shared_with_id']!, _sharedWithIdMeta));
+    } else if (isInserting) {
+      context.missing(_sharedWithIdMeta);
+    }
+    if (data.containsKey('in_timeline')) {
+      context.handle(
+          _inTimelineMeta,
+          inTimeline.isAcceptableOrUnknown(
+              data['in_timeline']!, _inTimelineMeta));
+    }
+    return context;
+  }
+
+  @override
+  Set<i0.GeneratedColumn> get $primaryKey => {sharedById, sharedWithId};
+  @override
+  i1.PartnerEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return i1.PartnerEntityData(
+      sharedById: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!,
+      sharedWithId: attachedDatabase.typeMapping.read(
+          i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!,
+      inTimeline: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!,
+    );
+  }
+
+  @override
+  $PartnerEntityTable createAlias(String alias) {
+    return $PartnerEntityTable(attachedDatabase, alias);
+  }
+
+  @override
+  bool get withoutRowId => true;
+  @override
+  bool get isStrict => true;
+}
+
+class PartnerEntityData extends i0.DataClass
+    implements i0.Insertable<i1.PartnerEntityData> {
+  final i2.Uint8List sharedById;
+  final i2.Uint8List sharedWithId;
+  final bool inTimeline;
+  const PartnerEntityData(
+      {required this.sharedById,
+      required this.sharedWithId,
+      required this.inTimeline});
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById);
+    map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId);
+    map['in_timeline'] = i0.Variable<bool>(inTimeline);
+    return map;
+  }
+
+  factory PartnerEntityData.fromJson(Map<String, dynamic> json,
+      {i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return PartnerEntityData(
+      sharedById: serializer.fromJson<i2.Uint8List>(json['sharedById']),
+      sharedWithId: serializer.fromJson<i2.Uint8List>(json['sharedWithId']),
+      inTimeline: serializer.fromJson<bool>(json['inTimeline']),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'sharedById': serializer.toJson<i2.Uint8List>(sharedById),
+      'sharedWithId': serializer.toJson<i2.Uint8List>(sharedWithId),
+      'inTimeline': serializer.toJson<bool>(inTimeline),
+    };
+  }
+
+  i1.PartnerEntityData copyWith(
+          {i2.Uint8List? sharedById,
+          i2.Uint8List? sharedWithId,
+          bool? inTimeline}) =>
+      i1.PartnerEntityData(
+        sharedById: sharedById ?? this.sharedById,
+        sharedWithId: sharedWithId ?? this.sharedWithId,
+        inTimeline: inTimeline ?? this.inTimeline,
+      );
+  PartnerEntityData copyWithCompanion(i1.PartnerEntityCompanion data) {
+    return PartnerEntityData(
+      sharedById:
+          data.sharedById.present ? data.sharedById.value : this.sharedById,
+      sharedWithId: data.sharedWithId.present
+          ? data.sharedWithId.value
+          : this.sharedWithId,
+      inTimeline:
+          data.inTimeline.present ? data.inTimeline.value : this.inTimeline,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('PartnerEntityData(')
+          ..write('sharedById: $sharedById, ')
+          ..write('sharedWithId: $sharedWithId, ')
+          ..write('inTimeline: $inTimeline')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById),
+      i0.$driftBlobEquality.hash(sharedWithId), inTimeline);
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is i1.PartnerEntityData &&
+          i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) &&
+          i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) &&
+          other.inTimeline == this.inTimeline);
+}
+
+class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
+  final i0.Value<i2.Uint8List> sharedById;
+  final i0.Value<i2.Uint8List> sharedWithId;
+  final i0.Value<bool> inTimeline;
+  const PartnerEntityCompanion({
+    this.sharedById = const i0.Value.absent(),
+    this.sharedWithId = const i0.Value.absent(),
+    this.inTimeline = const i0.Value.absent(),
+  });
+  PartnerEntityCompanion.insert({
+    required i2.Uint8List sharedById,
+    required i2.Uint8List sharedWithId,
+    this.inTimeline = const i0.Value.absent(),
+  })  : sharedById = i0.Value(sharedById),
+        sharedWithId = i0.Value(sharedWithId);
+  static i0.Insertable<i1.PartnerEntityData> custom({
+    i0.Expression<i2.Uint8List>? sharedById,
+    i0.Expression<i2.Uint8List>? sharedWithId,
+    i0.Expression<bool>? inTimeline,
+  }) {
+    return i0.RawValuesInsertable({
+      if (sharedById != null) 'shared_by_id': sharedById,
+      if (sharedWithId != null) 'shared_with_id': sharedWithId,
+      if (inTimeline != null) 'in_timeline': inTimeline,
+    });
+  }
+
+  i1.PartnerEntityCompanion copyWith(
+      {i0.Value<i2.Uint8List>? sharedById,
+      i0.Value<i2.Uint8List>? sharedWithId,
+      i0.Value<bool>? inTimeline}) {
+    return i1.PartnerEntityCompanion(
+      sharedById: sharedById ?? this.sharedById,
+      sharedWithId: sharedWithId ?? this.sharedWithId,
+      inTimeline: inTimeline ?? this.inTimeline,
+    );
+  }
+
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    if (sharedById.present) {
+      map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById.value);
+    }
+    if (sharedWithId.present) {
+      map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId.value);
+    }
+    if (inTimeline.present) {
+      map['in_timeline'] = i0.Variable<bool>(inTimeline.value);
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('PartnerEntityCompanion(')
+          ..write('sharedById: $sharedById, ')
+          ..write('sharedWithId: $sharedWithId, ')
+          ..write('inTimeline: $inTimeline')
+          ..write(')'))
+        .toString();
+  }
+}
diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart
index 710856d9f7..955b2267d1 100644
--- a/mobile/lib/infrastructure/entities/user.entity.dart
+++ b/mobile/lib/infrastructure/entities/user.entity.dart
@@ -1,4 +1,7 @@
+import 'package:drift/drift.dart' hide Index;
 import 'package:immich_mobile/domain/models/user.model.dart';
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
+import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
 import 'package:immich_mobile/utils/hash.dart';
 import 'package:isar/isar.dart';
 
@@ -71,3 +74,20 @@ class User {
         quotaSizeInBytes: quotaSizeInBytes,
       );
 }
+
+class UserEntity extends Table with DriftDefaultsMixin {
+  const UserEntity();
+
+  BlobColumn get id => blob()();
+  TextColumn get name => text()();
+  BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
+  TextColumn get email => text()();
+  TextColumn get profileImagePath => text().nullable()();
+  DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
+  // Quota
+  IntColumn get quotaSizeInBytes => integer().nullable()();
+  IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
+
+  @override
+  Set<Column> get primaryKey => {id};
+}
diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart
new file mode 100644
index 0000000000..474746a792
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart
@@ -0,0 +1,656 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
+    as i1;
+import 'dart:typed_data' as i2;
+import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
+
+typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
+    Function({
+  required i2.Uint8List id,
+  required String name,
+  i0.Value<bool> isAdmin,
+  required String email,
+  i0.Value<String?> profileImagePath,
+  i0.Value<DateTime> updatedAt,
+  i0.Value<int?> quotaSizeInBytes,
+  i0.Value<int> quotaUsageInBytes,
+});
+typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion
+    Function({
+  i0.Value<i2.Uint8List> id,
+  i0.Value<String> name,
+  i0.Value<bool> isAdmin,
+  i0.Value<String> email,
+  i0.Value<String?> profileImagePath,
+  i0.Value<DateTime> updatedAt,
+  i0.Value<int?> quotaSizeInBytes,
+  i0.Value<int> quotaUsageInBytes,
+});
+
+class $$UserEntityTableFilterComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
+  $$UserEntityTableFilterComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnFilters<i2.Uint8List> get id => $composableBuilder(
+      column: $table.id, builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<String> get name => $composableBuilder(
+      column: $table.name, builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<bool> get isAdmin => $composableBuilder(
+      column: $table.isAdmin, builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<String> get email => $composableBuilder(
+      column: $table.email, builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<String> get profileImagePath => $composableBuilder(
+      column: $table.profileImagePath,
+      builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
+      column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<int> get quotaSizeInBytes => $composableBuilder(
+      column: $table.quotaSizeInBytes,
+      builder: (column) => i0.ColumnFilters(column));
+
+  i0.ColumnFilters<int> get quotaUsageInBytes => $composableBuilder(
+      column: $table.quotaUsageInBytes,
+      builder: (column) => i0.ColumnFilters(column));
+}
+
+class $$UserEntityTableOrderingComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
+  $$UserEntityTableOrderingComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnOrderings<i2.Uint8List> get id => $composableBuilder(
+      column: $table.id, builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<String> get name => $composableBuilder(
+      column: $table.name, builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<bool> get isAdmin => $composableBuilder(
+      column: $table.isAdmin, builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<String> get email => $composableBuilder(
+      column: $table.email, builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<String> get profileImagePath => $composableBuilder(
+      column: $table.profileImagePath,
+      builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
+      column: $table.updatedAt,
+      builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<int> get quotaSizeInBytes => $composableBuilder(
+      column: $table.quotaSizeInBytes,
+      builder: (column) => i0.ColumnOrderings(column));
+
+  i0.ColumnOrderings<int> get quotaUsageInBytes => $composableBuilder(
+      column: $table.quotaUsageInBytes,
+      builder: (column) => i0.ColumnOrderings(column));
+}
+
+class $$UserEntityTableAnnotationComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserEntityTable> {
+  $$UserEntityTableAnnotationComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.GeneratedColumn<i2.Uint8List> get id =>
+      $composableBuilder(column: $table.id, builder: (column) => column);
+
+  i0.GeneratedColumn<String> get name =>
+      $composableBuilder(column: $table.name, builder: (column) => column);
+
+  i0.GeneratedColumn<bool> get isAdmin =>
+      $composableBuilder(column: $table.isAdmin, builder: (column) => column);
+
+  i0.GeneratedColumn<String> get email =>
+      $composableBuilder(column: $table.email, builder: (column) => column);
+
+  i0.GeneratedColumn<String> get profileImagePath => $composableBuilder(
+      column: $table.profileImagePath, builder: (column) => column);
+
+  i0.GeneratedColumn<DateTime> get updatedAt =>
+      $composableBuilder(column: $table.updatedAt, builder: (column) => column);
+
+  i0.GeneratedColumn<int> get quotaSizeInBytes => $composableBuilder(
+      column: $table.quotaSizeInBytes, builder: (column) => column);
+
+  i0.GeneratedColumn<int> get quotaUsageInBytes => $composableBuilder(
+      column: $table.quotaUsageInBytes, builder: (column) => column);
+}
+
+class $$UserEntityTableTableManager extends i0.RootTableManager<
+    i0.GeneratedDatabase,
+    i1.$UserEntityTable,
+    i1.UserEntityData,
+    i1.$$UserEntityTableFilterComposer,
+    i1.$$UserEntityTableOrderingComposer,
+    i1.$$UserEntityTableAnnotationComposer,
+    $$UserEntityTableCreateCompanionBuilder,
+    $$UserEntityTableUpdateCompanionBuilder,
+    (
+      i1.UserEntityData,
+      i0.BaseReferences<i0.GeneratedDatabase, i1.$UserEntityTable,
+          i1.UserEntityData>
+    ),
+    i1.UserEntityData,
+    i0.PrefetchHooks Function()> {
+  $$UserEntityTableTableManager(
+      i0.GeneratedDatabase db, i1.$UserEntityTable table)
+      : super(i0.TableManagerState(
+          db: db,
+          table: table,
+          createFilteringComposer: () =>
+              i1.$$UserEntityTableFilterComposer($db: db, $table: table),
+          createOrderingComposer: () =>
+              i1.$$UserEntityTableOrderingComposer($db: db, $table: table),
+          createComputedFieldComposer: () =>
+              i1.$$UserEntityTableAnnotationComposer($db: db, $table: table),
+          updateCompanionCallback: ({
+            i0.Value<i2.Uint8List> id = const i0.Value.absent(),
+            i0.Value<String> name = const i0.Value.absent(),
+            i0.Value<bool> isAdmin = const i0.Value.absent(),
+            i0.Value<String> email = const i0.Value.absent(),
+            i0.Value<String?> profileImagePath = const i0.Value.absent(),
+            i0.Value<DateTime> updatedAt = const i0.Value.absent(),
+            i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
+            i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
+          }) =>
+              i1.UserEntityCompanion(
+            id: id,
+            name: name,
+            isAdmin: isAdmin,
+            email: email,
+            profileImagePath: profileImagePath,
+            updatedAt: updatedAt,
+            quotaSizeInBytes: quotaSizeInBytes,
+            quotaUsageInBytes: quotaUsageInBytes,
+          ),
+          createCompanionCallback: ({
+            required i2.Uint8List id,
+            required String name,
+            i0.Value<bool> isAdmin = const i0.Value.absent(),
+            required String email,
+            i0.Value<String?> profileImagePath = const i0.Value.absent(),
+            i0.Value<DateTime> updatedAt = const i0.Value.absent(),
+            i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
+            i0.Value<int> quotaUsageInBytes = const i0.Value.absent(),
+          }) =>
+              i1.UserEntityCompanion.insert(
+            id: id,
+            name: name,
+            isAdmin: isAdmin,
+            email: email,
+            profileImagePath: profileImagePath,
+            updatedAt: updatedAt,
+            quotaSizeInBytes: quotaSizeInBytes,
+            quotaUsageInBytes: quotaUsageInBytes,
+          ),
+          withReferenceMapper: (p0) => p0
+              .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
+              .toList(),
+          prefetchHooksCallback: null,
+        ));
+}
+
+typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager<
+    i0.GeneratedDatabase,
+    i1.$UserEntityTable,
+    i1.UserEntityData,
+    i1.$$UserEntityTableFilterComposer,
+    i1.$$UserEntityTableOrderingComposer,
+    i1.$$UserEntityTableAnnotationComposer,
+    $$UserEntityTableCreateCompanionBuilder,
+    $$UserEntityTableUpdateCompanionBuilder,
+    (
+      i1.UserEntityData,
+      i0.BaseReferences<i0.GeneratedDatabase, i1.$UserEntityTable,
+          i1.UserEntityData>
+    ),
+    i1.UserEntityData,
+    i0.PrefetchHooks Function()>;
+
+class $UserEntityTable extends i3.UserEntity
+    with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
+  @override
+  final i0.GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  $UserEntityTable(this.attachedDatabase, [this._alias]);
+  static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
+  @override
+  late final i0.GeneratedColumn<i2.Uint8List> id =
+      i0.GeneratedColumn<i2.Uint8List>('id', aliasedName, false,
+          type: i0.DriftSqlType.blob, requiredDuringInsert: true);
+  static const i0.VerificationMeta _nameMeta =
+      const i0.VerificationMeta('name');
+  @override
+  late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
+      'name', aliasedName, false,
+      type: i0.DriftSqlType.string, requiredDuringInsert: true);
+  static const i0.VerificationMeta _isAdminMeta =
+      const i0.VerificationMeta('isAdmin');
+  @override
+  late final i0.GeneratedColumn<bool> isAdmin = i0.GeneratedColumn<bool>(
+      'is_admin', aliasedName, false,
+      type: i0.DriftSqlType.bool,
+      requiredDuringInsert: false,
+      defaultConstraints:
+          i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'),
+      defaultValue: const i4.Constant(false));
+  static const i0.VerificationMeta _emailMeta =
+      const i0.VerificationMeta('email');
+  @override
+  late final i0.GeneratedColumn<String> email = i0.GeneratedColumn<String>(
+      'email', aliasedName, false,
+      type: i0.DriftSqlType.string, requiredDuringInsert: true);
+  static const i0.VerificationMeta _profileImagePathMeta =
+      const i0.VerificationMeta('profileImagePath');
+  @override
+  late final i0.GeneratedColumn<String> profileImagePath =
+      i0.GeneratedColumn<String>('profile_image_path', aliasedName, true,
+          type: i0.DriftSqlType.string, requiredDuringInsert: false);
+  static const i0.VerificationMeta _updatedAtMeta =
+      const i0.VerificationMeta('updatedAt');
+  @override
+  late final i0.GeneratedColumn<DateTime> updatedAt =
+      i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
+          type: i0.DriftSqlType.dateTime,
+          requiredDuringInsert: false,
+          defaultValue: i4.currentDateAndTime);
+  static const i0.VerificationMeta _quotaSizeInBytesMeta =
+      const i0.VerificationMeta('quotaSizeInBytes');
+  @override
+  late final i0.GeneratedColumn<int> quotaSizeInBytes = i0.GeneratedColumn<int>(
+      'quota_size_in_bytes', aliasedName, true,
+      type: i0.DriftSqlType.int, requiredDuringInsert: false);
+  static const i0.VerificationMeta _quotaUsageInBytesMeta =
+      const i0.VerificationMeta('quotaUsageInBytes');
+  @override
+  late final i0.GeneratedColumn<int> quotaUsageInBytes =
+      i0.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
+          type: i0.DriftSqlType.int,
+          requiredDuringInsert: false,
+          defaultValue: const i4.Constant(0));
+  @override
+  List<i0.GeneratedColumn> get $columns => [
+        id,
+        name,
+        isAdmin,
+        email,
+        profileImagePath,
+        updatedAt,
+        quotaSizeInBytes,
+        quotaUsageInBytes
+      ];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'user_entity';
+  @override
+  i0.VerificationContext validateIntegrity(
+      i0.Insertable<i1.UserEntityData> instance,
+      {bool isInserting = false}) {
+    final context = i0.VerificationContext();
+    final data = instance.toColumns(true);
+    if (data.containsKey('id')) {
+      context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
+    } else if (isInserting) {
+      context.missing(_idMeta);
+    }
+    if (data.containsKey('name')) {
+      context.handle(
+          _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
+    } else if (isInserting) {
+      context.missing(_nameMeta);
+    }
+    if (data.containsKey('is_admin')) {
+      context.handle(_isAdminMeta,
+          isAdmin.isAcceptableOrUnknown(data['is_admin']!, _isAdminMeta));
+    }
+    if (data.containsKey('email')) {
+      context.handle(
+          _emailMeta, email.isAcceptableOrUnknown(data['email']!, _emailMeta));
+    } else if (isInserting) {
+      context.missing(_emailMeta);
+    }
+    if (data.containsKey('profile_image_path')) {
+      context.handle(
+          _profileImagePathMeta,
+          profileImagePath.isAcceptableOrUnknown(
+              data['profile_image_path']!, _profileImagePathMeta));
+    }
+    if (data.containsKey('updated_at')) {
+      context.handle(_updatedAtMeta,
+          updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
+    }
+    if (data.containsKey('quota_size_in_bytes')) {
+      context.handle(
+          _quotaSizeInBytesMeta,
+          quotaSizeInBytes.isAcceptableOrUnknown(
+              data['quota_size_in_bytes']!, _quotaSizeInBytesMeta));
+    }
+    if (data.containsKey('quota_usage_in_bytes')) {
+      context.handle(
+          _quotaUsageInBytesMeta,
+          quotaUsageInBytes.isAcceptableOrUnknown(
+              data['quota_usage_in_bytes']!, _quotaUsageInBytesMeta));
+    }
+    return context;
+  }
+
+  @override
+  Set<i0.GeneratedColumn> get $primaryKey => {id};
+  @override
+  i1.UserEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return i1.UserEntityData(
+      id: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!,
+      name: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
+      isAdmin: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_admin'])!,
+      email: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.string, data['${effectivePrefix}email'])!,
+      profileImagePath: attachedDatabase.typeMapping.read(
+          i0.DriftSqlType.string, data['${effectivePrefix}profile_image_path']),
+      updatedAt: attachedDatabase.typeMapping.read(
+          i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
+      quotaSizeInBytes: attachedDatabase.typeMapping.read(
+          i0.DriftSqlType.int, data['${effectivePrefix}quota_size_in_bytes']),
+      quotaUsageInBytes: attachedDatabase.typeMapping.read(
+          i0.DriftSqlType.int, data['${effectivePrefix}quota_usage_in_bytes'])!,
+    );
+  }
+
+  @override
+  $UserEntityTable createAlias(String alias) {
+    return $UserEntityTable(attachedDatabase, alias);
+  }
+
+  @override
+  bool get withoutRowId => true;
+  @override
+  bool get isStrict => true;
+}
+
+class UserEntityData extends i0.DataClass
+    implements i0.Insertable<i1.UserEntityData> {
+  final i2.Uint8List id;
+  final String name;
+  final bool isAdmin;
+  final String email;
+  final String? profileImagePath;
+  final DateTime updatedAt;
+  final int? quotaSizeInBytes;
+  final int quotaUsageInBytes;
+  const UserEntityData(
+      {required this.id,
+      required this.name,
+      required this.isAdmin,
+      required this.email,
+      this.profileImagePath,
+      required this.updatedAt,
+      this.quotaSizeInBytes,
+      required this.quotaUsageInBytes});
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    map['id'] = i0.Variable<i2.Uint8List>(id);
+    map['name'] = i0.Variable<String>(name);
+    map['is_admin'] = i0.Variable<bool>(isAdmin);
+    map['email'] = i0.Variable<String>(email);
+    if (!nullToAbsent || profileImagePath != null) {
+      map['profile_image_path'] = i0.Variable<String>(profileImagePath);
+    }
+    map['updated_at'] = i0.Variable<DateTime>(updatedAt);
+    if (!nullToAbsent || quotaSizeInBytes != null) {
+      map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes);
+    }
+    map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes);
+    return map;
+  }
+
+  factory UserEntityData.fromJson(Map<String, dynamic> json,
+      {i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return UserEntityData(
+      id: serializer.fromJson<i2.Uint8List>(json['id']),
+      name: serializer.fromJson<String>(json['name']),
+      isAdmin: serializer.fromJson<bool>(json['isAdmin']),
+      email: serializer.fromJson<String>(json['email']),
+      profileImagePath: serializer.fromJson<String?>(json['profileImagePath']),
+      updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
+      quotaSizeInBytes: serializer.fromJson<int?>(json['quotaSizeInBytes']),
+      quotaUsageInBytes: serializer.fromJson<int>(json['quotaUsageInBytes']),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'id': serializer.toJson<i2.Uint8List>(id),
+      'name': serializer.toJson<String>(name),
+      'isAdmin': serializer.toJson<bool>(isAdmin),
+      'email': serializer.toJson<String>(email),
+      'profileImagePath': serializer.toJson<String?>(profileImagePath),
+      'updatedAt': serializer.toJson<DateTime>(updatedAt),
+      'quotaSizeInBytes': serializer.toJson<int?>(quotaSizeInBytes),
+      'quotaUsageInBytes': serializer.toJson<int>(quotaUsageInBytes),
+    };
+  }
+
+  i1.UserEntityData copyWith(
+          {i2.Uint8List? id,
+          String? name,
+          bool? isAdmin,
+          String? email,
+          i0.Value<String?> profileImagePath = const i0.Value.absent(),
+          DateTime? updatedAt,
+          i0.Value<int?> quotaSizeInBytes = const i0.Value.absent(),
+          int? quotaUsageInBytes}) =>
+      i1.UserEntityData(
+        id: id ?? this.id,
+        name: name ?? this.name,
+        isAdmin: isAdmin ?? this.isAdmin,
+        email: email ?? this.email,
+        profileImagePath: profileImagePath.present
+            ? profileImagePath.value
+            : this.profileImagePath,
+        updatedAt: updatedAt ?? this.updatedAt,
+        quotaSizeInBytes: quotaSizeInBytes.present
+            ? quotaSizeInBytes.value
+            : this.quotaSizeInBytes,
+        quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
+      );
+  UserEntityData copyWithCompanion(i1.UserEntityCompanion data) {
+    return UserEntityData(
+      id: data.id.present ? data.id.value : this.id,
+      name: data.name.present ? data.name.value : this.name,
+      isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin,
+      email: data.email.present ? data.email.value : this.email,
+      profileImagePath: data.profileImagePath.present
+          ? data.profileImagePath.value
+          : this.profileImagePath,
+      updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
+      quotaSizeInBytes: data.quotaSizeInBytes.present
+          ? data.quotaSizeInBytes.value
+          : this.quotaSizeInBytes,
+      quotaUsageInBytes: data.quotaUsageInBytes.present
+          ? data.quotaUsageInBytes.value
+          : this.quotaUsageInBytes,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('UserEntityData(')
+          ..write('id: $id, ')
+          ..write('name: $name, ')
+          ..write('isAdmin: $isAdmin, ')
+          ..write('email: $email, ')
+          ..write('profileImagePath: $profileImagePath, ')
+          ..write('updatedAt: $updatedAt, ')
+          ..write('quotaSizeInBytes: $quotaSizeInBytes, ')
+          ..write('quotaUsageInBytes: $quotaUsageInBytes')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin,
+      email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes);
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is i1.UserEntityData &&
+          i0.$driftBlobEquality.equals(other.id, this.id) &&
+          other.name == this.name &&
+          other.isAdmin == this.isAdmin &&
+          other.email == this.email &&
+          other.profileImagePath == this.profileImagePath &&
+          other.updatedAt == this.updatedAt &&
+          other.quotaSizeInBytes == this.quotaSizeInBytes &&
+          other.quotaUsageInBytes == this.quotaUsageInBytes);
+}
+
+class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
+  final i0.Value<i2.Uint8List> id;
+  final i0.Value<String> name;
+  final i0.Value<bool> isAdmin;
+  final i0.Value<String> email;
+  final i0.Value<String?> profileImagePath;
+  final i0.Value<DateTime> updatedAt;
+  final i0.Value<int?> quotaSizeInBytes;
+  final i0.Value<int> quotaUsageInBytes;
+  const UserEntityCompanion({
+    this.id = const i0.Value.absent(),
+    this.name = const i0.Value.absent(),
+    this.isAdmin = const i0.Value.absent(),
+    this.email = const i0.Value.absent(),
+    this.profileImagePath = const i0.Value.absent(),
+    this.updatedAt = const i0.Value.absent(),
+    this.quotaSizeInBytes = const i0.Value.absent(),
+    this.quotaUsageInBytes = const i0.Value.absent(),
+  });
+  UserEntityCompanion.insert({
+    required i2.Uint8List id,
+    required String name,
+    this.isAdmin = const i0.Value.absent(),
+    required String email,
+    this.profileImagePath = const i0.Value.absent(),
+    this.updatedAt = const i0.Value.absent(),
+    this.quotaSizeInBytes = const i0.Value.absent(),
+    this.quotaUsageInBytes = const i0.Value.absent(),
+  })  : id = i0.Value(id),
+        name = i0.Value(name),
+        email = i0.Value(email);
+  static i0.Insertable<i1.UserEntityData> custom({
+    i0.Expression<i2.Uint8List>? id,
+    i0.Expression<String>? name,
+    i0.Expression<bool>? isAdmin,
+    i0.Expression<String>? email,
+    i0.Expression<String>? profileImagePath,
+    i0.Expression<DateTime>? updatedAt,
+    i0.Expression<int>? quotaSizeInBytes,
+    i0.Expression<int>? quotaUsageInBytes,
+  }) {
+    return i0.RawValuesInsertable({
+      if (id != null) 'id': id,
+      if (name != null) 'name': name,
+      if (isAdmin != null) 'is_admin': isAdmin,
+      if (email != null) 'email': email,
+      if (profileImagePath != null) 'profile_image_path': profileImagePath,
+      if (updatedAt != null) 'updated_at': updatedAt,
+      if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes,
+      if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes,
+    });
+  }
+
+  i1.UserEntityCompanion copyWith(
+      {i0.Value<i2.Uint8List>? id,
+      i0.Value<String>? name,
+      i0.Value<bool>? isAdmin,
+      i0.Value<String>? email,
+      i0.Value<String?>? profileImagePath,
+      i0.Value<DateTime>? updatedAt,
+      i0.Value<int?>? quotaSizeInBytes,
+      i0.Value<int>? quotaUsageInBytes}) {
+    return i1.UserEntityCompanion(
+      id: id ?? this.id,
+      name: name ?? this.name,
+      isAdmin: isAdmin ?? this.isAdmin,
+      email: email ?? this.email,
+      profileImagePath: profileImagePath ?? this.profileImagePath,
+      updatedAt: updatedAt ?? this.updatedAt,
+      quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
+      quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
+    );
+  }
+
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    if (id.present) {
+      map['id'] = i0.Variable<i2.Uint8List>(id.value);
+    }
+    if (name.present) {
+      map['name'] = i0.Variable<String>(name.value);
+    }
+    if (isAdmin.present) {
+      map['is_admin'] = i0.Variable<bool>(isAdmin.value);
+    }
+    if (email.present) {
+      map['email'] = i0.Variable<String>(email.value);
+    }
+    if (profileImagePath.present) {
+      map['profile_image_path'] = i0.Variable<String>(profileImagePath.value);
+    }
+    if (updatedAt.present) {
+      map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
+    }
+    if (quotaSizeInBytes.present) {
+      map['quota_size_in_bytes'] = i0.Variable<int>(quotaSizeInBytes.value);
+    }
+    if (quotaUsageInBytes.present) {
+      map['quota_usage_in_bytes'] = i0.Variable<int>(quotaUsageInBytes.value);
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('UserEntityCompanion(')
+          ..write('id: $id, ')
+          ..write('name: $name, ')
+          ..write('isAdmin: $isAdmin, ')
+          ..write('email: $email, ')
+          ..write('profileImagePath: $profileImagePath, ')
+          ..write('updatedAt: $updatedAt, ')
+          ..write('quotaSizeInBytes: $quotaSizeInBytes, ')
+          ..write('quotaUsageInBytes: $quotaUsageInBytes')
+          ..write(')'))
+        .toString();
+  }
+}
diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart
new file mode 100644
index 0000000000..ebbfeebadd
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart
@@ -0,0 +1,21 @@
+import 'package:drift/drift.dart';
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
+import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
+import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
+
+class UserMetadataEntity extends Table with DriftDefaultsMixin {
+  const UserMetadataEntity();
+
+  BlobColumn get userId =>
+      blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
+  TextColumn get preferences => text().map(userPreferenceConverter)();
+
+  @override
+  Set<Column> get primaryKey => {userId};
+}
+
+final JsonTypeConverter2<UserPreferences, String, Object?>
+    userPreferenceConverter = TypeConverter.json2(
+  fromJson: (json) => UserPreferences.fromMap(json as Map<String, Object?>),
+  toJson: (pref) => pref.toMap(),
+);
diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart
new file mode 100644
index 0000000000..9829fd1acc
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart
@@ -0,0 +1,468 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
+    as i1;
+import 'dart:typed_data' as i2;
+import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3;
+import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
+    as i4;
+import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
+    as i5;
+import 'package:drift/internal/modular.dart' as i6;
+
+typedef $$UserMetadataEntityTableCreateCompanionBuilder
+    = i1.UserMetadataEntityCompanion Function({
+  required i2.Uint8List userId,
+  required i3.UserPreferences preferences,
+});
+typedef $$UserMetadataEntityTableUpdateCompanionBuilder
+    = i1.UserMetadataEntityCompanion Function({
+  i0.Value<i2.Uint8List> userId,
+  i0.Value<i3.UserPreferences> preferences,
+});
+
+final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
+    i0.GeneratedDatabase,
+    i1.$UserMetadataEntityTable,
+    i1.UserMetadataEntityData> {
+  $$UserMetadataEntityTableReferences(
+      super.$_db, super.$_table, super.$_typedResult);
+
+  static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
+      i6.ReadDatabaseContainer(db)
+          .resultSet<i5.$UserEntityTable>('user_entity')
+          .createAlias(i0.$_aliasNameGenerator(
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i1.$UserMetadataEntityTable>(
+                      'user_metadata_entity')
+                  .userId,
+              i6.ReadDatabaseContainer(db)
+                  .resultSet<i5.$UserEntityTable>('user_entity')
+                  .id));
+
+  i5.$$UserEntityTableProcessedTableManager get userId {
+    final $_column = $_itemColumn<i2.Uint8List>('user_id')!;
+
+    final manager = i5
+        .$$UserEntityTableTableManager(
+            $_db,
+            i6.ReadDatabaseContainer($_db)
+                .resultSet<i5.$UserEntityTable>('user_entity'))
+        .filter((f) => f.id.sqlEquals($_column));
+    final item = $_typedResult.readTableOrNull(_userIdTable($_db));
+    if (item == null) return manager;
+    return i0.ProcessedTableManager(
+        manager.$state.copyWith(prefetchedData: [item]));
+  }
+}
+
+class $$UserMetadataEntityTableFilterComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
+  $$UserMetadataEntityTableFilterComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnWithTypeConverterFilters<i3.UserPreferences, i3.UserPreferences,
+          String>
+      get preferences => $composableBuilder(
+          column: $table.preferences,
+          builder: (column) => i0.ColumnWithTypeConverterFilters(column));
+
+  i5.$$UserEntityTableFilterComposer get userId {
+    final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.userId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableFilterComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$UserMetadataEntityTableOrderingComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
+  $$UserMetadataEntityTableOrderingComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.ColumnOrderings<String> get preferences => $composableBuilder(
+      column: $table.preferences,
+      builder: (column) => i0.ColumnOrderings(column));
+
+  i5.$$UserEntityTableOrderingComposer get userId {
+    final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.userId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableOrderingComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$UserMetadataEntityTableAnnotationComposer
+    extends i0.Composer<i0.GeneratedDatabase, i1.$UserMetadataEntityTable> {
+  $$UserMetadataEntityTableAnnotationComposer({
+    required super.$db,
+    required super.$table,
+    super.joinBuilder,
+    super.$addJoinBuilderToRootComposer,
+    super.$removeJoinBuilderFromRootComposer,
+  });
+  i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
+      get preferences => $composableBuilder(
+          column: $table.preferences, builder: (column) => column);
+
+  i5.$$UserEntityTableAnnotationComposer get userId {
+    final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
+        composer: this,
+        getCurrentColumn: (t) => t.userId,
+        referencedTable: i6.ReadDatabaseContainer($db)
+            .resultSet<i5.$UserEntityTable>('user_entity'),
+        getReferencedColumn: (t) => t.id,
+        builder: (joinBuilder,
+                {$addJoinBuilderToRootComposer,
+                $removeJoinBuilderFromRootComposer}) =>
+            i5.$$UserEntityTableAnnotationComposer(
+              $db: $db,
+              $table: i6.ReadDatabaseContainer($db)
+                  .resultSet<i5.$UserEntityTable>('user_entity'),
+              $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
+              joinBuilder: joinBuilder,
+              $removeJoinBuilderFromRootComposer:
+                  $removeJoinBuilderFromRootComposer,
+            ));
+    return composer;
+  }
+}
+
+class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
+    i0.GeneratedDatabase,
+    i1.$UserMetadataEntityTable,
+    i1.UserMetadataEntityData,
+    i1.$$UserMetadataEntityTableFilterComposer,
+    i1.$$UserMetadataEntityTableOrderingComposer,
+    i1.$$UserMetadataEntityTableAnnotationComposer,
+    $$UserMetadataEntityTableCreateCompanionBuilder,
+    $$UserMetadataEntityTableUpdateCompanionBuilder,
+    (i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences),
+    i1.UserMetadataEntityData,
+    i0.PrefetchHooks Function({bool userId})> {
+  $$UserMetadataEntityTableTableManager(
+      i0.GeneratedDatabase db, i1.$UserMetadataEntityTable table)
+      : super(i0.TableManagerState(
+          db: db,
+          table: table,
+          createFilteringComposer: () => i1
+              .$$UserMetadataEntityTableFilterComposer($db: db, $table: table),
+          createOrderingComposer: () =>
+              i1.$$UserMetadataEntityTableOrderingComposer(
+                  $db: db, $table: table),
+          createComputedFieldComposer: () =>
+              i1.$$UserMetadataEntityTableAnnotationComposer(
+                  $db: db, $table: table),
+          updateCompanionCallback: ({
+            i0.Value<i2.Uint8List> userId = const i0.Value.absent(),
+            i0.Value<i3.UserPreferences> preferences = const i0.Value.absent(),
+          }) =>
+              i1.UserMetadataEntityCompanion(
+            userId: userId,
+            preferences: preferences,
+          ),
+          createCompanionCallback: ({
+            required i2.Uint8List userId,
+            required i3.UserPreferences preferences,
+          }) =>
+              i1.UserMetadataEntityCompanion.insert(
+            userId: userId,
+            preferences: preferences,
+          ),
+          withReferenceMapper: (p0) => p0
+              .map((e) => (
+                    e.readTable(table),
+                    i1.$$UserMetadataEntityTableReferences(db, table, e)
+                  ))
+              .toList(),
+          prefetchHooksCallback: ({userId = false}) {
+            return i0.PrefetchHooks(
+              db: db,
+              explicitlyWatchedTables: [],
+              addJoins: <
+                  T extends i0.TableManagerState<
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic,
+                      dynamic>>(state) {
+                if (userId) {
+                  state = state.withJoin(
+                    currentTable: table,
+                    currentColumn: table.userId,
+                    referencedTable:
+                        i1.$$UserMetadataEntityTableReferences._userIdTable(db),
+                    referencedColumn: i1.$$UserMetadataEntityTableReferences
+                        ._userIdTable(db)
+                        .id,
+                  ) as T;
+                }
+
+                return state;
+              },
+              getPrefetchedDataCallback: (items) async {
+                return [];
+              },
+            );
+          },
+        ));
+}
+
+typedef $$UserMetadataEntityTableProcessedTableManager
+    = i0.ProcessedTableManager<
+        i0.GeneratedDatabase,
+        i1.$UserMetadataEntityTable,
+        i1.UserMetadataEntityData,
+        i1.$$UserMetadataEntityTableFilterComposer,
+        i1.$$UserMetadataEntityTableOrderingComposer,
+        i1.$$UserMetadataEntityTableAnnotationComposer,
+        $$UserMetadataEntityTableCreateCompanionBuilder,
+        $$UserMetadataEntityTableUpdateCompanionBuilder,
+        (i1.UserMetadataEntityData, i1.$$UserMetadataEntityTableReferences),
+        i1.UserMetadataEntityData,
+        i0.PrefetchHooks Function({bool userId})>;
+
+class $UserMetadataEntityTable extends i4.UserMetadataEntity
+    with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
+  @override
+  final i0.GeneratedDatabase attachedDatabase;
+  final String? _alias;
+  $UserMetadataEntityTable(this.attachedDatabase, [this._alias]);
+  static const i0.VerificationMeta _userIdMeta =
+      const i0.VerificationMeta('userId');
+  @override
+  late final i0.GeneratedColumn<i2.Uint8List> userId =
+      i0.GeneratedColumn<i2.Uint8List>('user_id', aliasedName, false,
+          type: i0.DriftSqlType.blob,
+          requiredDuringInsert: true,
+          defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
+              'REFERENCES user_entity (id) ON DELETE CASCADE'));
+  @override
+  late final i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
+      preferences = i0.GeneratedColumn<String>(
+              'preferences', aliasedName, false,
+              type: i0.DriftSqlType.string, requiredDuringInsert: true)
+          .withConverter<i3.UserPreferences>(
+              i1.$UserMetadataEntityTable.$converterpreferences);
+  @override
+  List<i0.GeneratedColumn> get $columns => [userId, preferences];
+  @override
+  String get aliasedName => _alias ?? actualTableName;
+  @override
+  String get actualTableName => $name;
+  static const String $name = 'user_metadata_entity';
+  @override
+  i0.VerificationContext validateIntegrity(
+      i0.Insertable<i1.UserMetadataEntityData> instance,
+      {bool isInserting = false}) {
+    final context = i0.VerificationContext();
+    final data = instance.toColumns(true);
+    if (data.containsKey('user_id')) {
+      context.handle(_userIdMeta,
+          userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta));
+    } else if (isInserting) {
+      context.missing(_userIdMeta);
+    }
+    return context;
+  }
+
+  @override
+  Set<i0.GeneratedColumn> get $primaryKey => {userId};
+  @override
+  i1.UserMetadataEntityData map(Map<String, dynamic> data,
+      {String? tablePrefix}) {
+    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+    return i1.UserMetadataEntityData(
+      userId: attachedDatabase.typeMapping
+          .read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!,
+      preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
+          attachedDatabase.typeMapping.read(
+              i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
+    );
+  }
+
+  @override
+  $UserMetadataEntityTable createAlias(String alias) {
+    return $UserMetadataEntityTable(attachedDatabase, alias);
+  }
+
+  static i0.JsonTypeConverter2<i3.UserPreferences, String, Object?>
+      $converterpreferences = i4.userPreferenceConverter;
+  @override
+  bool get withoutRowId => true;
+  @override
+  bool get isStrict => true;
+}
+
+class UserMetadataEntityData extends i0.DataClass
+    implements i0.Insertable<i1.UserMetadataEntityData> {
+  final i2.Uint8List userId;
+  final i3.UserPreferences preferences;
+  const UserMetadataEntityData(
+      {required this.userId, required this.preferences});
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    map['user_id'] = i0.Variable<i2.Uint8List>(userId);
+    {
+      map['preferences'] = i0.Variable<String>(
+          i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
+    }
+    return map;
+  }
+
+  factory UserMetadataEntityData.fromJson(Map<String, dynamic> json,
+      {i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return UserMetadataEntityData(
+      userId: serializer.fromJson<i2.Uint8List>(json['userId']),
+      preferences: i1.$UserMetadataEntityTable.$converterpreferences
+          .fromJson(serializer.fromJson<Object?>(json['preferences'])),
+    );
+  }
+  @override
+  Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
+    serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+    return <String, dynamic>{
+      'userId': serializer.toJson<i2.Uint8List>(userId),
+      'preferences': serializer.toJson<Object?>(i1
+          .$UserMetadataEntityTable.$converterpreferences
+          .toJson(preferences)),
+    };
+  }
+
+  i1.UserMetadataEntityData copyWith(
+          {i2.Uint8List? userId, i3.UserPreferences? preferences}) =>
+      i1.UserMetadataEntityData(
+        userId: userId ?? this.userId,
+        preferences: preferences ?? this.preferences,
+      );
+  UserMetadataEntityData copyWithCompanion(
+      i1.UserMetadataEntityCompanion data) {
+    return UserMetadataEntityData(
+      userId: data.userId.present ? data.userId.value : this.userId,
+      preferences:
+          data.preferences.present ? data.preferences.value : this.preferences,
+    );
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('UserMetadataEntityData(')
+          ..write('userId: $userId, ')
+          ..write('preferences: $preferences')
+          ..write(')'))
+        .toString();
+  }
+
+  @override
+  int get hashCode =>
+      Object.hash(i0.$driftBlobEquality.hash(userId), preferences);
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      (other is i1.UserMetadataEntityData &&
+          i0.$driftBlobEquality.equals(other.userId, this.userId) &&
+          other.preferences == this.preferences);
+}
+
+class UserMetadataEntityCompanion
+    extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
+  final i0.Value<i2.Uint8List> userId;
+  final i0.Value<i3.UserPreferences> preferences;
+  const UserMetadataEntityCompanion({
+    this.userId = const i0.Value.absent(),
+    this.preferences = const i0.Value.absent(),
+  });
+  UserMetadataEntityCompanion.insert({
+    required i2.Uint8List userId,
+    required i3.UserPreferences preferences,
+  })  : userId = i0.Value(userId),
+        preferences = i0.Value(preferences);
+  static i0.Insertable<i1.UserMetadataEntityData> custom({
+    i0.Expression<i2.Uint8List>? userId,
+    i0.Expression<String>? preferences,
+  }) {
+    return i0.RawValuesInsertable({
+      if (userId != null) 'user_id': userId,
+      if (preferences != null) 'preferences': preferences,
+    });
+  }
+
+  i1.UserMetadataEntityCompanion copyWith(
+      {i0.Value<i2.Uint8List>? userId,
+      i0.Value<i3.UserPreferences>? preferences}) {
+    return i1.UserMetadataEntityCompanion(
+      userId: userId ?? this.userId,
+      preferences: preferences ?? this.preferences,
+    );
+  }
+
+  @override
+  Map<String, i0.Expression> toColumns(bool nullToAbsent) {
+    final map = <String, i0.Expression>{};
+    if (userId.present) {
+      map['user_id'] = i0.Variable<i2.Uint8List>(userId.value);
+    }
+    if (preferences.present) {
+      map['preferences'] = i0.Variable<String>(i1
+          .$UserMetadataEntityTable.$converterpreferences
+          .toSql(preferences.value));
+    }
+    return map;
+  }
+
+  @override
+  String toString() {
+    return (StringBuffer('UserMetadataEntityCompanion(')
+          ..write('userId: $userId, ')
+          ..write('preferences: $preferences')
+          ..write(')'))
+        .toString();
+  }
+}
diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart
index 74e182bdee..997714e1b6 100644
--- a/mobile/lib/infrastructure/repositories/db.repository.dart
+++ b/mobile/lib/infrastructure/repositories/db.repository.dart
@@ -1,8 +1,15 @@
 import 'dart:async';
 
+import 'package:drift/drift.dart';
+import 'package:drift_flutter/drift_flutter.dart';
 import 'package:immich_mobile/domain/interfaces/db.interface.dart';
+import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
+import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
+import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
 import 'package:isar/isar.dart';
 
+import 'db.repository.drift.dart';
+
 // #zoneTxn is the symbol used by Isar to mark a transaction within the current zone
 // ref: isar/isar_common.dart
 const Symbol _kzoneTxn = #zoneTxn;
@@ -17,3 +24,35 @@ class IsarDatabaseRepository implements IDatabaseRepository {
   Future<T> transaction<T>(Future<T> Function() callback) =>
       Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback();
 }
+
+@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity])
+class Drift extends $Drift implements IDatabaseRepository {
+  Drift([QueryExecutor? executor])
+      : super(
+          executor ??
+              driftDatabase(
+                name: 'immich',
+                native: const DriftNativeOptions(shareAcrossIsolates: true),
+              ),
+        );
+
+  @override
+  int get schemaVersion => 1;
+
+  @override
+  MigrationStrategy get migration => MigrationStrategy(
+        beforeOpen: (details) async {
+          await customStatement('PRAGMA journal_mode = WAL');
+          await customStatement('PRAGMA foreign_keys = ON');
+        },
+      );
+}
+
+class DriftDatabaseRepository implements IDatabaseRepository {
+  final Drift _db;
+  const DriftDatabaseRepository(this._db);
+
+  @override
+  Future<T> transaction<T>(Future<T> Function() callback) =>
+      _db.transaction(callback);
+}
diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart
new file mode 100644
index 0000000000..a4c2b31dcd
--- /dev/null
+++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart
@@ -0,0 +1,67 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
+    as i1;
+import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
+    as i2;
+import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
+    as i3;
+
+abstract class $Drift extends i0.GeneratedDatabase {
+  $Drift(i0.QueryExecutor e) : super(e);
+  $DriftManager get managers => $DriftManager(this);
+  late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
+  late final i2.$UserMetadataEntityTable userMetadataEntity =
+      i2.$UserMetadataEntityTable(this);
+  late final i3.$PartnerEntityTable partnerEntity =
+      i3.$PartnerEntityTable(this);
+  @override
+  Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
+      allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
+  @override
+  List<i0.DatabaseSchemaEntity> get allSchemaEntities =>
+      [userEntity, userMetadataEntity, partnerEntity];
+  @override
+  i0.StreamQueryUpdateRules get streamUpdateRules =>
+      const i0.StreamQueryUpdateRules(
+        [
+          i0.WritePropagation(
+            on: i0.TableUpdateQuery.onTableName('user_entity',
+                limitUpdateKind: i0.UpdateKind.delete),
+            result: [
+              i0.TableUpdate('user_metadata_entity',
+                  kind: i0.UpdateKind.delete),
+            ],
+          ),
+          i0.WritePropagation(
+            on: i0.TableUpdateQuery.onTableName('user_entity',
+                limitUpdateKind: i0.UpdateKind.delete),
+            result: [
+              i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
+            ],
+          ),
+          i0.WritePropagation(
+            on: i0.TableUpdateQuery.onTableName('user_entity',
+                limitUpdateKind: i0.UpdateKind.delete),
+            result: [
+              i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
+            ],
+          ),
+        ],
+      );
+  @override
+  i0.DriftDatabaseOptions get options =>
+      const i0.DriftDatabaseOptions(storeDateTimeAsText: true);
+}
+
+class $DriftManager {
+  final $Drift _db;
+  $DriftManager(this._db);
+  i1.$$UserEntityTableTableManager get userEntity =>
+      i1.$$UserEntityTableTableManager(_db, _db.userEntity);
+  i2.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
+      i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
+  i3.$$PartnerEntityTableTableManager get partnerEntity =>
+      i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
+}
diff --git a/mobile/lib/infrastructure/utils/drift_default.mixin.dart b/mobile/lib/infrastructure/utils/drift_default.mixin.dart
new file mode 100644
index 0000000000..1ba4589ed5
--- /dev/null
+++ b/mobile/lib/infrastructure/utils/drift_default.mixin.dart
@@ -0,0 +1,9 @@
+import 'package:drift/drift.dart';
+
+mixin DriftDefaultsMixin on Table {
+  @override
+  bool get isStrict => true;
+
+  @override
+  bool get withoutRowId => true;
+}
diff --git a/mobile/lib/infrastructure/utils/exif.converter.dart b/mobile/lib/infrastructure/utils/exif.converter.dart
index 0f6e2b0295..eb9945f454 100644
--- a/mobile/lib/infrastructure/utils/exif.converter.dart
+++ b/mobile/lib/infrastructure/utils/exif.converter.dart
@@ -1,6 +1,7 @@
 import 'package:immich_mobile/domain/models/exif.model.dart';
 import 'package:openapi/api.dart';
 
+// TODO: Move to repository once all classes are refactored
 abstract final class ExifDtoConverter {
   static ExifInfo fromDto(ExifResponseDto dto) {
     return ExifInfo(
diff --git a/mobile/lib/infrastructure/utils/user.converter.dart b/mobile/lib/infrastructure/utils/user.converter.dart
index fcf7ede51c..eb7b24737e 100644
--- a/mobile/lib/infrastructure/utils/user.converter.dart
+++ b/mobile/lib/infrastructure/utils/user.converter.dart
@@ -1,6 +1,8 @@
 import 'package:immich_mobile/domain/models/user.model.dart';
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
 import 'package:openapi/api.dart';
 
+// TODO: Move to repository once all classes are refactored
 abstract final class UserConverter {
   /// Base user dto used where the complete user object is not required
   static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart
index b3727e8323..8866cb01b0 100644
--- a/mobile/lib/widgets/common/user_circle_avatar.dart
+++ b/mobile/lib/widgets/common/user_circle_avatar.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/domain/models/store.model.dart';
 import 'package:immich_mobile/domain/models/user.model.dart';
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
 import 'package:immich_mobile/entities/store.entity.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/services/api.service.dart';
diff --git a/mobile/makefile b/mobile/makefile
index 43bc59c7d4..0931d6c167 100644
--- a/mobile/makefile
+++ b/mobile/makefile
@@ -14,3 +14,6 @@ create_splash:
 
 build_release_android:
 	flutter build appbundle
+
+migrations:
+	dart run drift_dev make-migrations
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 9c841a870e..e79d9f4084 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -206,6 +206,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.4.0"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
   checked_yaml:
     dependency: transitive
     description:
@@ -382,6 +390,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.2"
+  drift:
+    dependency: "direct main"
+    description:
+      name: drift
+      sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.26.0"
+  drift_dev:
+    dependency: "direct dev"
+    description:
+      name: drift_dev
+      sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.26.0"
+  drift_flutter:
+    dependency: "direct main"
+    description:
+      name: drift_flutter
+      sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.4"
   dynamic_color:
     dependency: "direct main"
     description:
@@ -1288,6 +1320,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.5.0"
+  recase:
+    dependency: transitive
+    description:
+      name: recase
+      sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.0"
   riverpod:
     dependency: transitive
     description:
@@ -1549,6 +1589,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.4.0"
+  sqlite3:
+    dependency: transitive
+    description:
+      name: sqlite3
+      sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.7.5"
+  sqlite3_flutter_libs:
+    dependency: transitive
+    description:
+      name: sqlite3_flutter_libs
+      sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.5.31"
+  sqlparser:
+    dependency: transitive
+    description:
+      name: sqlparser
+      sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.41.0"
   stack_trace:
     dependency: transitive
     description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index a778f804d0..d4ab110a3e 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -73,6 +73,9 @@ dependencies:
   isar_flutter_libs: # contains Isar Core
     version: *isar_version
     hosted: https://pub.isar-community.dev/
+  # DB
+  drift: ^2.23.1
+  drift_flutter: ^0.2.4
 
 dependency_overrides:
   analyzer: ^6.0.0
@@ -99,6 +102,8 @@ dev_dependencies:
   immich_mobile_immich_lint:
     path: './immich_lint'
   fake_async: ^1.3.1
+  # Drift generator
+  drift_dev: ^2.23.1
 
 flutter:
   uses-material-design: true
diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart
index 1dfec9b4b1..764342520f 100644
--- a/mobile/test/fixtures/user.stub.dart
+++ b/mobile/test/fixtures/user.stub.dart
@@ -1,4 +1,5 @@
 import 'package:immich_mobile/domain/models/user.model.dart';
+import 'package:immich_mobile/domain/models/user_metadata.model.dart';
 
 abstract final class UserStub {
   const UserStub._();