diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts
index bb838bbae3..f3e0530696 100644
--- a/e2e/src/api/specs/person.e2e-spec.ts
+++ b/e2e/src/api/specs/person.e2e-spec.ts
@@ -1,7 +1,7 @@
-import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
+import { getPerson, LoginResponseDto, PersonResponseDto } from '@immich/sdk';
 import { uuidDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
-import { app, utils } from 'src/utils';
+import { app, asBearerAuth, utils } from 'src/utils';
 import request from 'supertest';
 import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 
@@ -203,6 +203,22 @@ describe('/people', () => {
         birthDate: '1990-01-01T00:00:00.000Z',
       });
     });
+
+    it('should create a favorite person', async () => {
+      const { status, body } = await request(app)
+        .post(`/people`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({
+          name: 'New Favorite Person',
+          isFavorite: true,
+        });
+      expect(status).toBe(201);
+      expect(body).toMatchObject({
+        id: expect.any(String),
+        name: 'New Favorite Person',
+        isFavorite: true,
+      });
+    });
   });
 
   describe('PUT /people/:id', () => {
@@ -216,6 +232,7 @@ describe('/people', () => {
       { key: 'name', type: 'string' },
       { key: 'featureFaceAssetId', type: 'string' },
       { key: 'isHidden', type: 'boolean value' },
+      { key: 'isFavorite', type: 'boolean value' },
     ]) {
       it(`should not allow null ${key}`, async () => {
         const { status, body } = await request(app)
@@ -255,6 +272,24 @@ describe('/people', () => {
       expect(status).toBe(200);
       expect(body).toMatchObject({ birthDate: null });
     });
+
+    it('should mark a person as favorite', async () => {
+      const person = await utils.createPerson(admin.accessToken, {
+        name: 'visible_person',
+      });
+
+      expect(person.isFavorite).toBe(false);
+
+      const { status, body } = await request(app)
+        .put(`/people/${person.id}`)
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({ isFavorite: true });
+      expect(status).toBe(200);
+      expect(body).toMatchObject({ isFavorite: true });
+
+      const person2 = await getPerson({ id: person.id }, { headers: asBearerAuth(admin.accessToken) });
+      expect(person2).toMatchObject({ id: person.id, isFavorite: true });
+    });
   });
 
   describe('POST /people/:id/merge', () => {
diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart
index 042e4fa36f..6f8e312959 100644
--- a/mobile/openapi/lib/model/people_update_item.dart
+++ b/mobile/openapi/lib/model/people_update_item.dart
@@ -16,6 +16,7 @@ class PeopleUpdateItem {
     this.birthDate,
     this.featureFaceAssetId,
     required this.id,
+    this.isFavorite,
     this.isHidden,
     this.name,
   });
@@ -35,6 +36,14 @@ class PeopleUpdateItem {
   /// Person id.
   String id;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? isFavorite;
+
   /// Person visibility
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -58,6 +67,7 @@ class PeopleUpdateItem {
     other.birthDate == birthDate &&
     other.featureFaceAssetId == featureFaceAssetId &&
     other.id == id &&
+    other.isFavorite == isFavorite &&
     other.isHidden == isHidden &&
     other.name == name;
 
@@ -67,11 +77,12 @@ class PeopleUpdateItem {
     (birthDate == null ? 0 : birthDate!.hashCode) +
     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
     (id.hashCode) +
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isHidden == null ? 0 : isHidden!.hashCode) +
     (name == null ? 0 : name!.hashCode);
 
   @override
-  String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name]';
+  String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -86,6 +97,11 @@ class PeopleUpdateItem {
     //  json[r'featureFaceAssetId'] = null;
     }
       json[r'id'] = this.id;
+    if (this.isFavorite != null) {
+      json[r'isFavorite'] = this.isFavorite;
+    } else {
+    //  json[r'isFavorite'] = null;
+    }
     if (this.isHidden != null) {
       json[r'isHidden'] = this.isHidden;
     } else {
@@ -111,6 +127,7 @@ class PeopleUpdateItem {
         birthDate: mapDateTime(json, r'birthDate', r''),
         featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
         id: mapValueOfType<String>(json, r'id')!,
+        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isHidden: mapValueOfType<bool>(json, r'isHidden'),
         name: mapValueOfType<String>(json, r'name'),
       );
diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart
index 36bd6dfee9..bc1d67c240 100644
--- a/mobile/openapi/lib/model/person_create_dto.dart
+++ b/mobile/openapi/lib/model/person_create_dto.dart
@@ -14,6 +14,7 @@ class PersonCreateDto {
   /// Returns a new [PersonCreateDto] instance.
   PersonCreateDto({
     this.birthDate,
+    this.isFavorite,
     this.isHidden,
     this.name,
   });
@@ -21,6 +22,14 @@ class PersonCreateDto {
   /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
   DateTime? birthDate;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? isFavorite;
+
   /// Person visibility
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -42,6 +51,7 @@ class PersonCreateDto {
   @override
   bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto &&
     other.birthDate == birthDate &&
+    other.isFavorite == isFavorite &&
     other.isHidden == isHidden &&
     other.name == name;
 
@@ -49,11 +59,12 @@ class PersonCreateDto {
   int get hashCode =>
     // ignore: unnecessary_parenthesis
     (birthDate == null ? 0 : birthDate!.hashCode) +
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isHidden == null ? 0 : isHidden!.hashCode) +
     (name == null ? 0 : name!.hashCode);
 
   @override
-  String toString() => 'PersonCreateDto[birthDate=$birthDate, isHidden=$isHidden, name=$name]';
+  String toString() => 'PersonCreateDto[birthDate=$birthDate, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -62,6 +73,11 @@ class PersonCreateDto {
     } else {
     //  json[r'birthDate'] = null;
     }
+    if (this.isFavorite != null) {
+      json[r'isFavorite'] = this.isFavorite;
+    } else {
+    //  json[r'isFavorite'] = null;
+    }
     if (this.isHidden != null) {
       json[r'isHidden'] = this.isHidden;
     } else {
@@ -85,6 +101,7 @@ class PersonCreateDto {
 
       return PersonCreateDto(
         birthDate: mapDateTime(json, r'birthDate', r''),
+        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isHidden: mapValueOfType<bool>(json, r'isHidden'),
         name: mapValueOfType<String>(json, r'name'),
       );
diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart
index 0b36fcde3b..1884459928 100644
--- a/mobile/openapi/lib/model/person_response_dto.dart
+++ b/mobile/openapi/lib/model/person_response_dto.dart
@@ -15,6 +15,7 @@ class PersonResponseDto {
   PersonResponseDto({
     required this.birthDate,
     required this.id,
+    this.isFavorite,
     required this.isHidden,
     required this.name,
     required this.thumbnailPath,
@@ -25,6 +26,15 @@ class PersonResponseDto {
 
   String id;
 
+  /// This property was added in v1.126.0
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? isFavorite;
+
   bool isHidden;
 
   String name;
@@ -44,6 +54,7 @@ class PersonResponseDto {
   bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
     other.birthDate == birthDate &&
     other.id == id &&
+    other.isFavorite == isFavorite &&
     other.isHidden == isHidden &&
     other.name == name &&
     other.thumbnailPath == thumbnailPath &&
@@ -54,13 +65,14 @@ class PersonResponseDto {
     // ignore: unnecessary_parenthesis
     (birthDate == null ? 0 : birthDate!.hashCode) +
     (id.hashCode) +
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isHidden.hashCode) +
     (name.hashCode) +
     (thumbnailPath.hashCode) +
     (updatedAt == null ? 0 : updatedAt!.hashCode);
 
   @override
-  String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
+  String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -70,6 +82,11 @@ class PersonResponseDto {
     //  json[r'birthDate'] = null;
     }
       json[r'id'] = this.id;
+    if (this.isFavorite != null) {
+      json[r'isFavorite'] = this.isFavorite;
+    } else {
+    //  json[r'isFavorite'] = null;
+    }
       json[r'isHidden'] = this.isHidden;
       json[r'name'] = this.name;
       json[r'thumbnailPath'] = this.thumbnailPath;
@@ -92,6 +109,7 @@ class PersonResponseDto {
       return PersonResponseDto(
         birthDate: mapDateTime(json, r'birthDate', r''),
         id: mapValueOfType<String>(json, r'id')!,
+        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isHidden: mapValueOfType<bool>(json, r'isHidden')!,
         name: mapValueOfType<String>(json, r'name')!,
         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart
index 51a7ea25d0..cf0688a27f 100644
--- a/mobile/openapi/lib/model/person_update_dto.dart
+++ b/mobile/openapi/lib/model/person_update_dto.dart
@@ -15,6 +15,7 @@ class PersonUpdateDto {
   PersonUpdateDto({
     this.birthDate,
     this.featureFaceAssetId,
+    this.isFavorite,
     this.isHidden,
     this.name,
   });
@@ -31,6 +32,14 @@ class PersonUpdateDto {
   ///
   String? featureFaceAssetId;
 
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? isFavorite;
+
   /// Person visibility
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -53,6 +62,7 @@ class PersonUpdateDto {
   bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
     other.birthDate == birthDate &&
     other.featureFaceAssetId == featureFaceAssetId &&
+    other.isFavorite == isFavorite &&
     other.isHidden == isHidden &&
     other.name == name;
 
@@ -61,11 +71,12 @@ class PersonUpdateDto {
     // ignore: unnecessary_parenthesis
     (birthDate == null ? 0 : birthDate!.hashCode) +
     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isHidden == null ? 0 : isHidden!.hashCode) +
     (name == null ? 0 : name!.hashCode);
 
   @override
-  String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name]';
+  String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -79,6 +90,11 @@ class PersonUpdateDto {
     } else {
     //  json[r'featureFaceAssetId'] = null;
     }
+    if (this.isFavorite != null) {
+      json[r'isFavorite'] = this.isFavorite;
+    } else {
+    //  json[r'isFavorite'] = null;
+    }
     if (this.isHidden != null) {
       json[r'isHidden'] = this.isHidden;
     } else {
@@ -103,6 +119,7 @@ class PersonUpdateDto {
       return PersonUpdateDto(
         birthDate: mapDateTime(json, r'birthDate', r''),
         featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
+        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isHidden: mapValueOfType<bool>(json, r'isHidden'),
         name: mapValueOfType<String>(json, r'name'),
       );
diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart
index b14bad7895..7d61db11f3 100644
--- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart
+++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart
@@ -16,6 +16,7 @@ class PersonWithFacesResponseDto {
     required this.birthDate,
     this.faces = const [],
     required this.id,
+    this.isFavorite,
     required this.isHidden,
     required this.name,
     required this.thumbnailPath,
@@ -28,6 +29,15 @@ class PersonWithFacesResponseDto {
 
   String id;
 
+  /// This property was added in v1.126.0
+  ///
+  /// Please note: This property should have been non-nullable! Since the specification file
+  /// does not include a default value (using the "default:" property), however, the generated
+  /// source code must fall back to having a nullable type.
+  /// Consider adding a "default:" property in the specification file to hide this note.
+  ///
+  bool? isFavorite;
+
   bool isHidden;
 
   String name;
@@ -48,6 +58,7 @@ class PersonWithFacesResponseDto {
     other.birthDate == birthDate &&
     _deepEquality.equals(other.faces, faces) &&
     other.id == id &&
+    other.isFavorite == isFavorite &&
     other.isHidden == isHidden &&
     other.name == name &&
     other.thumbnailPath == thumbnailPath &&
@@ -59,13 +70,14 @@ class PersonWithFacesResponseDto {
     (birthDate == null ? 0 : birthDate!.hashCode) +
     (faces.hashCode) +
     (id.hashCode) +
+    (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isHidden.hashCode) +
     (name.hashCode) +
     (thumbnailPath.hashCode) +
     (updatedAt == null ? 0 : updatedAt!.hashCode);
 
   @override
-  String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
+  String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -76,6 +88,11 @@ class PersonWithFacesResponseDto {
     }
       json[r'faces'] = this.faces;
       json[r'id'] = this.id;
+    if (this.isFavorite != null) {
+      json[r'isFavorite'] = this.isFavorite;
+    } else {
+    //  json[r'isFavorite'] = null;
+    }
       json[r'isHidden'] = this.isHidden;
       json[r'name'] = this.name;
       json[r'thumbnailPath'] = this.thumbnailPath;
@@ -99,6 +116,7 @@ class PersonWithFacesResponseDto {
         birthDate: mapDateTime(json, r'birthDate', r''),
         faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
         id: mapValueOfType<String>(json, r'id')!,
+        isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isHidden: mapValueOfType<bool>(json, r'isHidden')!,
         name: mapValueOfType<String>(json, r'name')!,
         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 85dc55aed8..f1ef466df4 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -10294,6 +10294,9 @@
             "description": "Person id.",
             "type": "string"
           },
+          "isFavorite": {
+            "type": "boolean"
+          },
           "isHidden": {
             "description": "Person visibility",
             "type": "boolean"
@@ -10399,6 +10402,9 @@
             "nullable": true,
             "type": "string"
           },
+          "isFavorite": {
+            "type": "boolean"
+          },
           "isHidden": {
             "description": "Person visibility",
             "type": "boolean"
@@ -10420,6 +10426,10 @@
           "id": {
             "type": "string"
           },
+          "isFavorite": {
+            "description": "This property was added in v1.126.0",
+            "type": "boolean"
+          },
           "isHidden": {
             "type": "boolean"
           },
@@ -10467,6 +10477,9 @@
             "description": "Asset is used to get the feature face thumbnail.",
             "type": "string"
           },
+          "isFavorite": {
+            "type": "boolean"
+          },
           "isHidden": {
             "description": "Person visibility",
             "type": "boolean"
@@ -10494,6 +10507,10 @@
           "id": {
             "type": "string"
           },
+          "isFavorite": {
+            "description": "This property was added in v1.126.0",
+            "type": "boolean"
+          },
           "isHidden": {
             "type": "boolean"
           },
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 32cca2dbce..6704a83cc7 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -215,6 +215,8 @@ export type PersonWithFacesResponseDto = {
     birthDate: string | null;
     faces: AssetFaceWithoutPersonResponseDto[];
     id: string;
+    /** This property was added in v1.126.0 */
+    isFavorite?: boolean;
     isHidden: boolean;
     name: string;
     thumbnailPath: string;
@@ -492,6 +494,8 @@ export type DuplicateResponseDto = {
 export type PersonResponseDto = {
     birthDate: string | null;
     id: string;
+    /** This property was added in v1.126.0 */
+    isFavorite?: boolean;
     isHidden: boolean;
     name: string;
     thumbnailPath: string;
@@ -689,6 +693,7 @@ export type PersonCreateDto = {
     /** Person date of birth.
     Note: the mobile app cannot currently set the birth date to null. */
     birthDate?: string | null;
+    isFavorite?: boolean;
     /** Person visibility */
     isHidden?: boolean;
     /** Person name. */
@@ -702,6 +707,7 @@ export type PeopleUpdateItem = {
     featureFaceAssetId?: string;
     /** Person id. */
     id: string;
+    isFavorite?: boolean;
     /** Person visibility */
     isHidden?: boolean;
     /** Person name. */
@@ -716,6 +722,7 @@ export type PersonUpdateDto = {
     birthDate?: string | null;
     /** Asset is used to get the feature face thumbnail. */
     featureFaceAssetId?: string;
+    isFavorite?: boolean;
     /** Person visibility */
     isHidden?: boolean;
     /** Person name. */
diff --git a/server/src/db.d.ts b/server/src/db.d.ts
index 6242914bee..16f73c53e7 100644
--- a/server/src/db.d.ts
+++ b/server/src/db.d.ts
@@ -279,6 +279,7 @@ export interface Person {
   createdAt: Generated<Timestamp>;
   faceAssetId: string | null;
   id: Generated<string>;
+  isFavorite: Generated<boolean>;
   isHidden: Generated<boolean>;
   name: Generated<string>;
   ownerId: string;
@@ -327,11 +328,6 @@ export interface SocketIoAttachments {
   payload: Buffer | null;
 }
 
-export interface SystemConfig {
-  key: string;
-  value: string | null;
-}
-
 export interface SystemMetadata {
   key: string;
   value: Json;
@@ -357,6 +353,15 @@ export interface TagsClosure {
   id_descendant: string;
 }
 
+export interface TypeormMetadata {
+  database: string | null;
+  name: string | null;
+  schema: string | null;
+  table: string | null;
+  type: string;
+  value: string | null;
+}
+
 export interface UserMetadata {
   key: string;
   userId: string;
@@ -431,11 +436,11 @@ export interface DB {
   shared_links: SharedLinks;
   smart_search: SmartSearch;
   socket_io_attachments: SocketIoAttachments;
-  system_config: SystemConfig;
   system_metadata: SystemMetadata;
   tag_asset: TagAsset;
   tags: Tags;
   tags_closure: TagsClosure;
+  typeorm_metadata: TypeormMetadata;
   user_metadata: UserMetadata;
   users: Users;
   "vectors.pg_vector_index_stat": VectorsPgVectorIndexStat;
diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts
index 047ef600b8..8bf041be37 100644
--- a/server/src/dtos/person.dto.ts
+++ b/server/src/dtos/person.dto.ts
@@ -32,6 +32,9 @@ export class PersonCreateDto {
    */
   @ValidateBoolean({ optional: true })
   isHidden?: boolean;
+
+  @ValidateBoolean({ optional: true })
+  isFavorite?: boolean;
 }
 
 export class PersonUpdateDto extends PersonCreateDto {
@@ -97,6 +100,8 @@ export class PersonResponseDto {
   isHidden!: boolean;
   @PropertyLifecycle({ addedAt: 'v1.107.0' })
   updatedAt?: Date;
+  @PropertyLifecycle({ addedAt: 'v1.126.0' })
+  isFavorite?: boolean;
 }
 
 export class PersonWithFacesResponseDto extends PersonResponseDto {
@@ -170,6 +175,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
     birthDate: person.birthDate,
     thumbnailPath: person.thumbnailPath,
     isHidden: person.isHidden,
+    isFavorite: person.isFavorite,
     updatedAt: person.updatedAt,
   };
 }
diff --git a/server/src/entities/person.entity.ts b/server/src/entities/person.entity.ts
index 5efbcbfa0b..8cf416b766 100644
--- a/server/src/entities/person.entity.ts
+++ b/server/src/entities/person.entity.ts
@@ -49,4 +49,7 @@ export class PersonEntity {
 
   @Column({ default: false })
   isHidden!: boolean;
+
+  @Column({ default: false })
+  isFavorite!: boolean;
 }
diff --git a/server/src/migrations/1734879118272-AddIsFavoritePerson.ts b/server/src/migrations/1734879118272-AddIsFavoritePerson.ts
new file mode 100644
index 0000000000..6f7640f96f
--- /dev/null
+++ b/server/src/migrations/1734879118272-AddIsFavoritePerson.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddIsFavoritePerson1734879118272 implements MigrationInterface {
+    name = 'AddIsFavoritePerson1734879118272'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "person" ADD "isFavorite" boolean NOT NULL DEFAULT false`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "isFavorite"`);
+    }
+
+}
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 7c2512aa26..73fb8313d2 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -132,6 +132,7 @@ export class PersonRepository implements IPersonRepository {
       )
       .where('person.ownerId', '=', userId)
       .orderBy('person.isHidden', 'asc')
+      .orderBy('person.isFavorite', 'desc')
       .having((eb) =>
         eb.or([
           eb('person.name', '!=', ''),
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index dc9d7a9329..5407821fab 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -30,6 +30,7 @@ const responseDto: PersonResponseDto = {
   thumbnailPath: '/path/to/thumbnail.jpg',
   isHidden: false,
   updatedAt: expect.any(Date),
+  isFavorite: false,
 };
 
 const statistics = { assets: 3 };
@@ -116,6 +117,7 @@ describe(PersonService.name, () => {
             birthDate: null,
             thumbnailPath: '/path/to/thumbnail.jpg',
             isHidden: true,
+            isFavorite: false,
             updatedAt: expect.any(Date),
           },
         ],
@@ -125,6 +127,35 @@ describe(PersonService.name, () => {
         withHidden: true,
       });
     });
+
+    it('should get all visible people and favorites should be first in the array', async () => {
+      personMock.getAllForUser.mockResolvedValue({
+        items: [personStub.isFavorite, personStub.withName],
+        hasNextPage: false,
+      });
+      personMock.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
+      await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({
+        hasNextPage: false,
+        total: 2,
+        hidden: 1,
+        people: [
+          {
+            id: 'person-4',
+            name: personStub.isFavorite.name,
+            birthDate: null,
+            thumbnailPath: '/path/to/thumbnail.jpg',
+            isHidden: false,
+            isFavorite: true,
+            updatedAt: expect.any(Date),
+          },
+          responseDto,
+        ],
+      });
+      expect(personMock.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
+        minimumFaceCount: 3,
+        withHidden: false,
+      });
+    });
   });
 
   describe('getById', () => {
@@ -227,6 +258,7 @@ describe(PersonService.name, () => {
         birthDate: '1976-06-30',
         thumbnailPath: '/path/to/thumbnail.jpg',
         isHidden: false,
+        isFavorite: false,
         updatedAt: expect.any(Date),
       });
       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
@@ -245,6 +277,16 @@ describe(PersonService.name, () => {
       expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
     });
 
+    it('should update a person favorite status', async () => {
+      personMock.update.mockResolvedValue(personStub.withName);
+      accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
+
+      await expect(sut.update(authStub.admin, 'person-1', { isFavorite: true })).resolves.toEqual(responseDto);
+
+      expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isFavorite: true });
+      expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
+    });
+
     it("should update a person's thumbnailPath", async () => {
       personMock.update.mockResolvedValue(personStub.withName);
       personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
@@ -375,6 +417,7 @@ describe(PersonService.name, () => {
       ).resolves.toEqual({
         birthDate: personStub.noName.birthDate,
         isHidden: personStub.noName.isHidden,
+        isFavorite: personStub.noName.isFavorite,
         id: personStub.noName.id,
         name: personStub.noName.name,
         thumbnailPath: personStub.noName.thumbnailPath,
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index bcc65cfad3..2f4a6bb0d1 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -184,13 +184,14 @@ export class PersonService extends BaseService {
       name: dto.name,
       birthDate: dto.birthDate,
       isHidden: dto.isHidden,
+      isFavorite: dto.isFavorite,
     });
   }
 
   async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
     await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
 
-    const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
+    const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite } = dto;
     // TODO: set by faceId directly
     let faceId: string | undefined = undefined;
     if (assetId) {
@@ -203,7 +204,14 @@ export class PersonService extends BaseService {
       faceId = face.id;
     }
 
-    const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
+    const person = await this.personRepository.update({
+      id,
+      faceAssetId: faceId,
+      name,
+      birthDate,
+      isHidden,
+      isFavorite,
+    });
 
     if (assetId) {
       await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
@@ -221,6 +229,7 @@ export class PersonService extends BaseService {
           name: person.name,
           birthDate: person.birthDate,
           featureFaceAssetId: person.featureFaceAssetId,
+          isFavorite: person.isFavorite,
         });
         results.push({ id: person.id, success: true });
       } catch (error: Error | any) {
diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts
index 544894b31e..ecd5b0dbea 100644
--- a/server/test/fixtures/person.stub.ts
+++ b/server/test/fixtures/person.stub.ts
@@ -15,6 +15,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   hidden: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -29,6 +30,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: true,
+    isFavorite: false,
   }),
   withName: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -43,6 +45,7 @@ export const personStub = {
     faceAssetId: 'assetFaceId',
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   withBirthDate: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -57,6 +60,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   noThumbnail: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -71,6 +75,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   newThumbnail: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -85,6 +90,7 @@ export const personStub = {
     faceAssetId: 'asset-id',
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   primaryPerson: Object.freeze<PersonEntity>({
     id: 'person-1',
@@ -99,6 +105,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   mergePerson: Object.freeze<PersonEntity>({
     id: 'person-2',
@@ -113,6 +120,7 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
   }),
   randomPerson: Object.freeze<PersonEntity>({
     id: 'person-3',
@@ -127,5 +135,21 @@ export const personStub = {
     faceAssetId: null,
     faceAsset: null,
     isHidden: false,
+    isFavorite: false,
+  }),
+  isFavorite: Object.freeze<PersonEntity>({
+    id: 'person-4',
+    createdAt: new Date('2021-01-01'),
+    updatedAt: new Date('2021-01-01'),
+    ownerId: userStub.admin.id,
+    owner: userStub.admin,
+    name: 'Person 1',
+    birthDate: null,
+    thumbnailPath: '/path/to/thumbnail.jpg',
+    faces: [],
+    faceAssetId: 'assetFaceId',
+    faceAsset: null,
+    isHidden: false,
+    isFavorite: true,
   }),
 };
diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte
index a83d1180f9..494dd94666 100644
--- a/web/src/lib/components/faces-page/people-card.svelte
+++ b/web/src/lib/components/faces-page/people-card.svelte
@@ -1,4 +1,7 @@
 <script lang="ts">
+  import { focusOutside } from '$lib/actions/focus-outside';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
   import { getPeopleThumbnailUrl } from '$lib/utils';
   import { type PersonResponseDto } from '@immich/sdk';
@@ -8,12 +11,13 @@
     mdiCalendarEditOutline,
     mdiDotsVertical,
     mdiEyeOffOutline,
+    mdiHeart,
+    mdiHeartMinusOutline,
+    mdiHeartOutline,
   } from '@mdi/js';
+  import { t } from 'svelte-i18n';
   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
   import MenuOption from '../shared-components/context-menu/menu-option.svelte';
-  import { t } from 'svelte-i18n';
-  import { focusOutside } from '$lib/actions/focus-outside';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
 
   interface Props {
     person: PersonResponseDto;
@@ -22,9 +26,18 @@
     onSetBirthDate: () => void;
     onMergePeople: () => void;
     onHidePerson: () => void;
+    onToggleFavorite: () => void;
   }
 
-  let { person, preload = false, onChangeName, onSetBirthDate, onMergePeople, onHidePerson }: Props = $props();
+  let {
+    person,
+    preload = false,
+    onChangeName,
+    onSetBirthDate,
+    onMergePeople,
+    onHidePerson,
+    onToggleFavorite,
+  }: Props = $props();
 
   let showVerticalDots = $state(false);
 </script>
@@ -51,6 +64,11 @@
         title={person.name}
         widthStyle="100%"
       />
+      {#if person.isFavorite}
+        <div class="absolute bottom-2 left-2 z-10">
+          <Icon path={mdiHeart} size="24" class="text-white" />
+        </div>
+      {/if}
     </div>
     {#if person.name}
       <span
@@ -76,6 +94,11 @@
         <MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
         <MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
         <MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
+        <MenuOption
+          onClick={onToggleFavorite}
+          icon={person.isFavorite ? mdiHeartMinusOutline : mdiHeartOutline}
+          text={person.isFavorite ? $t('unfavorite') : $t('to_favorite')}
+        />
       </ButtonContextMenu>
     </div>
   {/if}
diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte
index fef6a29b85..40a02f7425 100644
--- a/web/src/routes/(user)/explore/+page.svelte
+++ b/web/src/routes/(user)/explore/+page.svelte
@@ -11,6 +11,8 @@
   import { onMount } from 'svelte';
   import { websocketEvents } from '$lib/stores/websocket';
   import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { mdiHeart } from '@mdi/js';
 
   interface Props {
     data: PageData;
@@ -53,7 +55,7 @@
       <SingleGridRow class="grid md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4">
         {#snippet children({ itemCount })}
           {#each people.slice(0, itemCount) as person (person.id)}
-            <a href="{AppRoute.PEOPLE}/{person.id}" class="text-center">
+            <a href="{AppRoute.PEOPLE}/{person.id}" class="text-center relative">
               <ImageThumbnail
                 circle
                 shadow
@@ -61,6 +63,11 @@
                 altText={person.name}
                 widthStyle="100%"
               />
+              {#if person.isFavorite}
+                <div class="absolute bottom-2 left-2 z-10">
+                  <Icon path={mdiHeart} size="24" class="text-white" />
+                </div>
+              {/if}
               <p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
             </a>
           {/each}
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index cdfd0045c6..f3ea5d8638 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -222,6 +222,25 @@
     }
   };
 
+  const handleToggleFavorite = async (detail: PersonResponseDto) => {
+    try {
+      const updatedPerson = await updatePerson({
+        id: detail.id,
+        personUpdateDto: { isFavorite: !detail.isFavorite },
+      });
+
+      const index = people.findIndex((person) => person.id === detail.id);
+      people[index] = updatedPerson;
+
+      notificationController.show({
+        message: updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'),
+        type: NotificationType.Info,
+      });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: detail.isFavorite } }));
+    }
+  };
+
   const handleMergePeople = async (detail: PersonResponseDto) => {
     await goto(
       `${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`,
@@ -413,6 +432,7 @@
           onSetBirthDate={() => handleSetBirthDate(person)}
           onMergePeople={() => handleMergePeople(person)}
           onHidePerson={() => handleHidePerson(person)}
+          onToggleFavorite={() => handleToggleFavorite(person)}
         />
       {/snippet}
     </PeopleInfiniteScroll>
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 00a5284452..edaf33487c 100644
--- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,6 +1,8 @@
 <script lang="ts">
-  import { afterNavigate, goto } from '$app/navigation';
+  import { afterNavigate, goto, invalidateAll } from '$app/navigation';
   import { page } from '$app/stores';
+  import { clickOutside } from '$lib/actions/click-outside';
+  import { listNavigation } from '$lib/actions/list-navigation';
   import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
   import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
@@ -17,8 +19,10 @@
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
+  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
@@ -27,11 +31,12 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets.store';
+  import { preferences } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
-  import { clickOutside } from '$lib/actions/click-outside';
   import { handleError } from '$lib/utils/handle-error';
   import { isExternalUrl } from '$lib/utils/navigation';
   import {
@@ -50,16 +55,13 @@
     mdiDotsVertical,
     mdiEyeOffOutline,
     mdiEyeOutline,
+    mdiHeartMinusOutline,
+    mdiHeartOutline,
     mdiPlus,
   } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
-  import type { PageData } from './$types';
-  import { listNavigation } from '$lib/actions/list-navigation';
   import { t } from 'svelte-i18n';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
-  import { preferences } from '$lib/stores/user.store';
-  import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
-  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -181,6 +183,25 @@
     }
   };
 
+  const handleToggleFavorite = async () => {
+    try {
+      const updatedPerson = await updatePerson({
+        id: person.id,
+        personUpdateDto: { isFavorite: !person.isFavorite },
+      });
+
+      // Invalidate to reload the page data and have the favorite status updated
+      await invalidateAll();
+
+      notificationController.show({
+        message: updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'),
+        type: NotificationType.Info,
+      });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: person.isFavorite } }));
+    }
+  };
+
   const handleMerge = async (person: PersonResponseDto) => {
     await updateAssetCount();
     await handleGoBack();
@@ -445,6 +466,11 @@
               icon={mdiAccountMultipleCheckOutline}
               onClick={() => (viewMode = PersonPageViewMode.MERGE_PEOPLE)}
             />
+            <MenuOption
+              icon={person.isFavorite ? mdiHeartMinusOutline : mdiHeartOutline}
+              text={person.isFavorite ? $t('unfavorite') : $t('to_favorite')}
+              onClick={handleToggleFavorite}
+            />
           </ButtonContextMenu>
         {/snippet}
       </ControlAppBar>