diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
index 97e53d4a75..8196186059 100644
--- a/e2e/src/api/specs/asset.e2e-spec.ts
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -3,6 +3,7 @@ import {
   AssetMediaStatus,
   AssetResponseDto,
   AssetTypeEnum,
+  AssetVisibility,
   getAssetInfo,
   getMyUser,
   LoginResponseDto,
@@ -119,9 +120,9 @@ describe('/asset', () => {
       // stats
       utils.createAsset(statsUser.accessToken),
       utils.createAsset(statsUser.accessToken, { isFavorite: true }),
-      utils.createAsset(statsUser.accessToken, { isArchived: true }),
+      utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }),
       utils.createAsset(statsUser.accessToken, {
-        isArchived: true,
+        visibility: AssetVisibility.Archive,
         isFavorite: true,
         assetData: { filename: 'example.mp4' },
       }),
@@ -309,7 +310,7 @@ describe('/asset', () => {
       });
 
       it('disallows viewing archived assets', async () => {
-        const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
+        const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive });
 
         const { status } = await request(app)
           .get(`/assets/${asset.id}`)
@@ -353,7 +354,7 @@ describe('/asset', () => {
       const { status, body } = await request(app)
         .get('/assets/statistics')
         .set('Authorization', `Bearer ${statsUser.accessToken}`)
-        .query({ isArchived: true });
+        .query({ visibility: AssetVisibility.Archive });
 
       expect(status).toBe(200);
       expect(body).toEqual({ images: 1, videos: 1, total: 2 });
@@ -363,7 +364,7 @@ describe('/asset', () => {
       const { status, body } = await request(app)
         .get('/assets/statistics')
         .set('Authorization', `Bearer ${statsUser.accessToken}`)
-        .query({ isFavorite: true, isArchived: true });
+        .query({ isFavorite: true, visibility: AssetVisibility.Archive });
 
       expect(status).toBe(200);
       expect(body).toEqual({ images: 0, videos: 1, total: 1 });
@@ -373,7 +374,7 @@ describe('/asset', () => {
       const { status, body } = await request(app)
         .get('/assets/statistics')
         .set('Authorization', `Bearer ${statsUser.accessToken}`)
-        .query({ isFavorite: false, isArchived: false });
+        .query({ isFavorite: false, visibility: AssetVisibility.Timeline });
 
       expect(status).toBe(200);
       expect(body).toEqual({ images: 1, videos: 0, total: 1 });
@@ -459,7 +460,7 @@ describe('/asset', () => {
       const { status, body } = await request(app)
         .put(`/assets/${user1Assets[0].id}`)
         .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ isArchived: true });
+        .send({ visibility: AssetVisibility.Archive });
       expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
       expect(status).toEqual(200);
     });
diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/api/specs/map.e2e-spec.ts
index da5f779cff..977638aa24 100644
--- a/e2e/src/api/specs/map.e2e-spec.ts
+++ b/e2e/src/api/specs/map.e2e-spec.ts
@@ -1,4 +1,4 @@
-import { LoginResponseDto } from '@immich/sdk';
+import { AssetVisibility, LoginResponseDto } from '@immich/sdk';
 import { readFile } from 'node:fs/promises';
 import { basename, join } from 'node:path';
 import { Socket } from 'socket.io-client';
@@ -44,7 +44,7 @@ describe('/map', () => {
     it('should get map markers for all non-archived assets', async () => {
       const { status, body } = await request(app)
         .get('/map/markers')
-        .query({ isArchived: false })
+        .query({ visibility: AssetVisibility.Timeline })
         .set('Authorization', `Bearer ${admin.accessToken}`);
 
       expect(status).toBe(200);
diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts
index 23f8c65fa8..2f6ea75f77 100644
--- a/e2e/src/api/specs/search.e2e-spec.ts
+++ b/e2e/src/api/specs/search.e2e-spec.ts
@@ -1,4 +1,11 @@
-import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
+import {
+  AssetMediaResponseDto,
+  AssetResponseDto,
+  AssetVisibility,
+  deleteAssets,
+  LoginResponseDto,
+  updateAsset,
+} from '@immich/sdk';
 import { DateTime } from 'luxon';
 import { readFile } from 'node:fs/promises';
 import { join } from 'node:path';
@@ -49,7 +56,7 @@ describe('/search', () => {
       { filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
       { filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
 
-      { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
+      { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } },
 
       // used for search suggestions
       { filename: '/formats/png/density_plot.png' },
@@ -171,12 +178,12 @@ describe('/search', () => {
         deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
       },
       {
-        should: 'should search by isArchived (true)',
-        deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
+        should: 'should search by visibility (AssetVisibility.Archive)',
+        deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }),
       },
       {
-        should: 'should search by isArchived (false)',
-        deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
+        should: 'should search by visibility (AssetVisibility.Timeline)',
+        deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }),
       },
       {
         should: 'should search by type (image)',
@@ -185,7 +192,7 @@ describe('/search', () => {
       {
         should: 'should search by type (video)',
         deferred: () => ({
-          dto: { type: 'VIDEO' },
+          dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden },
           assets: [
             // the three live motion photos
             { id: expect.any(String) },
@@ -229,13 +236,6 @@ describe('/search', () => {
         should: 'should search by takenAfter (no results)',
         deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
       },
-      //   {
-      //     should: 'should search by originalPath',
-      //     deferred: () => ({
-      //       dto: { originalPath: asset1.originalPath },
-      //       assets: [asset1],
-      //     }),
-      //   },
       {
         should: 'should search by originalFilename',
         deferred: () => ({
@@ -265,7 +265,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             city: '',
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             includeNull: true,
           },
           assets: [assetLast],
@@ -276,7 +276,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             city: null,
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             includeNull: true,
           },
           assets: [assetLast],
@@ -297,7 +297,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             state: '',
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             withExif: true,
             includeNull: true,
           },
@@ -309,7 +309,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             state: null,
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             includeNull: true,
           },
           assets: [assetLast, assetNotocactus],
@@ -330,7 +330,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             country: '',
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             includeNull: true,
           },
           assets: [assetLast],
@@ -341,7 +341,7 @@ describe('/search', () => {
         deferred: () => ({
           dto: {
             country: null,
-            isVisible: true,
+            visibility: AssetVisibility.Timeline,
             includeNull: true,
           },
           assets: [assetLast],
diff --git a/e2e/src/api/specs/timeline.e2e-spec.ts b/e2e/src/api/specs/timeline.e2e-spec.ts
index bf330e994a..93ba8b6527 100644
--- a/e2e/src/api/specs/timeline.e2e-spec.ts
+++ b/e2e/src/api/specs/timeline.e2e-spec.ts
@@ -1,4 +1,4 @@
-import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
+import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
 import { DateTime } from 'luxon';
 import { createUserDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
@@ -104,7 +104,7 @@ describe('/timeline', () => {
       const req1 = await request(app)
         .get('/timeline/buckets')
         .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
-        .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
+        .query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive });
 
       expect(req1.status).toBe(400);
       expect(req1.body).toEqual(errorDto.badRequest());
@@ -112,7 +112,7 @@ describe('/timeline', () => {
       const req2 = await request(app)
         .get('/timeline/buckets')
         .set('Authorization', `Bearer ${user.accessToken}`)
-        .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
+        .query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined });
 
       expect(req2.status).toBe(400);
       expect(req2.body).toEqual(errorDto.badRequest());
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index 08b29a4a11..1d5004d385 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -3,6 +3,7 @@ import {
   AssetMediaCreateDto,
   AssetMediaResponseDto,
   AssetResponseDto,
+  AssetVisibility,
   CheckExistingAssetsDto,
   CreateAlbumDto,
   CreateLibraryDto,
@@ -429,7 +430,10 @@ export const utils = {
   },
 
   archiveAssets: (accessToken: string, ids: string[]) =>
-    updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }),
+    updateAssets(
+      { assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } },
+      { headers: asBearerAuth(accessToken) },
+    ),
 
   deleteAssets: (accessToken: string, ids: string[]) =>
     deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index 4bf62eca31..8a24e72fbe 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -197,7 +197,7 @@ class AssetService {
         ids: assets.map((e) => e.remoteId!).toList(),
         dateTimeOriginal: updateAssetDto.dateTimeOriginal,
         isFavorite: updateAssetDto.isFavorite,
-        isArchived: updateAssetDto.isArchived,
+        visibility: updateAssetDto.visibility,
         latitude: updateAssetDto.latitude,
         longitude: updateAssetDto.longitude,
       ),
@@ -229,7 +229,13 @@ class AssetService {
     bool isArchived,
   ) async {
     try {
-      await updateAssets(assets, UpdateAssetDto(isArchived: isArchived));
+      await updateAssets(
+        assets,
+        UpdateAssetDto(
+          visibility:
+              isArchived ? AssetVisibility.archive : AssetVisibility.timeline,
+        ),
+      );
 
       for (var element in assets) {
         element.isArchived = isArchived;
diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart
index 44ace78852..bcf67889c0 100644
--- a/mobile/lib/services/search.service.dart
+++ b/mobile/lib/services/search.service.dart
@@ -68,7 +68,9 @@ class SearchService {
             model: filter.camera.model,
             takenAfter: filter.date.takenAfter,
             takenBefore: filter.date.takenBefore,
-            isArchived: filter.display.isArchive ? true : null,
+            visibility: filter.display.isArchive
+                ? AssetVisibility.archive
+                : AssetVisibility.timeline,
             isFavorite: filter.display.isFavorite ? true : null,
             isNotInAlbum: filter.display.isNotInAlbum ? true : null,
             personIds: filter.people.map((e) => e.id).toList(),
@@ -95,7 +97,9 @@ class SearchService {
             model: filter.camera.model,
             takenAfter: filter.date.takenAfter,
             takenBefore: filter.date.takenBefore,
-            isArchived: filter.display.isArchive ? true : null,
+            visibility: filter.display.isArchive
+                ? AssetVisibility.archive
+                : AssetVisibility.timeline,
             isFavorite: filter.display.isFavorite ? true : null,
             isNotInAlbum: filter.display.isNotInAlbum ? true : null,
             personIds: filter.people.map((e) => e.id).toList(),
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 7c8afb09e4..5395f46801 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -302,6 +302,7 @@ Class | Method | HTTP request | Description
  - [AssetStackResponseDto](doc//AssetStackResponseDto.md)
  - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
  - [AssetTypeEnum](doc//AssetTypeEnum.md)
+ - [AssetVisibility](doc//AssetVisibility.md)
  - [AudioCodec](doc//AudioCodec.md)
  - [AvatarUpdate](doc//AvatarUpdate.md)
  - [BulkIdResponseDto](doc//BulkIdResponseDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index ab9b251e01..9a806a3f20 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -106,6 +106,7 @@ part 'model/asset_response_dto.dart';
 part 'model/asset_stack_response_dto.dart';
 part 'model/asset_stats_response_dto.dart';
 part 'model/asset_type_enum.dart';
+part 'model/asset_visibility.dart';
 part 'model/audio_codec.dart';
 part 'model/avatar_update.dart';
 part 'model/bulk_id_response_dto.dart';
diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart
index f744988449..06965e1f8b 100644
--- a/mobile/openapi/lib/api/assets_api.dart
+++ b/mobile/openapi/lib/api/assets_api.dart
@@ -342,12 +342,12 @@ class AssetsApi {
   /// Performs an HTTP 'GET /assets/statistics' operation and returns the [Response].
   /// Parameters:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
-  Future<Response> getAssetStatisticsWithHttpInfo({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
+  ///
+  /// * [AssetVisibility] visibility:
+  Future<Response> getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
     // ignore: prefer_const_declarations
     final apiPath = r'/assets/statistics';
 
@@ -358,15 +358,15 @@ class AssetsApi {
     final headerParams = <String, String>{};
     final formParams = <String, String>{};
 
-    if (isArchived != null) {
-      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
-    }
     if (isFavorite != null) {
       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
     }
     if (isTrashed != null) {
       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
     }
+    if (visibility != null) {
+      queryParams.addAll(_queryParams('', 'visibility', visibility));
+    }
 
     const contentTypes = <String>[];
 
@@ -384,13 +384,13 @@ class AssetsApi {
 
   /// Parameters:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
-  Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
-    final response = await getAssetStatisticsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, );
+  ///
+  /// * [AssetVisibility] visibility:
+  Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
+    final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
@@ -788,16 +788,14 @@ class AssetsApi {
   ///
   /// * [String] duration:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
-  /// * [bool] isVisible:
-  ///
   /// * [String] livePhotoVideoId:
   ///
   /// * [MultipartFile] sidecarData:
-  Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
+  ///
+  /// * [AssetVisibility] visibility:
+  Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
     // ignore: prefer_const_declarations
     final apiPath = r'/assets';
 
@@ -845,18 +843,10 @@ class AssetsApi {
       hasFields = true;
       mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
     }
-    if (isArchived != null) {
-      hasFields = true;
-      mp.fields[r'isArchived'] = parameterToString(isArchived);
-    }
     if (isFavorite != null) {
       hasFields = true;
       mp.fields[r'isFavorite'] = parameterToString(isFavorite);
     }
-    if (isVisible != null) {
-      hasFields = true;
-      mp.fields[r'isVisible'] = parameterToString(isVisible);
-    }
     if (livePhotoVideoId != null) {
       hasFields = true;
       mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId);
@@ -866,6 +856,10 @@ class AssetsApi {
       mp.fields[r'sidecarData'] = sidecarData.field;
       mp.files.add(sidecarData);
     }
+    if (visibility != null) {
+      hasFields = true;
+      mp.fields[r'visibility'] = parameterToString(visibility);
+    }
     if (hasFields) {
       postBody = mp;
     }
@@ -900,17 +894,15 @@ class AssetsApi {
   ///
   /// * [String] duration:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
-  /// * [bool] isVisible:
-  ///
   /// * [String] livePhotoVideoId:
   ///
   /// * [MultipartFile] sidecarData:
-  Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
-    final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt,  key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, );
+  ///
+  /// * [AssetVisibility] visibility:
+  Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
+    final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt,  key: key, xImmichChecksum: xImmichChecksum, duration: duration, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart
index 7ea7189b00..1d25a379e8 100644
--- a/mobile/openapi/lib/api/timeline_api.dart
+++ b/mobile/openapi/lib/api/timeline_api.dart
@@ -25,8 +25,6 @@ class TimelineApi {
   ///
   /// * [String] albumId:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
@@ -41,10 +39,12 @@ class TimelineApi {
   ///
   /// * [String] userId:
   ///
+  /// * [AssetVisibility] visibility:
+  ///
   /// * [bool] withPartners:
   ///
   /// * [bool] withStacked:
-  Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
+  Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
     // ignore: prefer_const_declarations
     final apiPath = r'/timeline/bucket';
 
@@ -58,9 +58,6 @@ class TimelineApi {
     if (albumId != null) {
       queryParams.addAll(_queryParams('', 'albumId', albumId));
     }
-    if (isArchived != null) {
-      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
-    }
     if (isFavorite != null) {
       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
     }
@@ -84,6 +81,9 @@ class TimelineApi {
     if (userId != null) {
       queryParams.addAll(_queryParams('', 'userId', userId));
     }
+    if (visibility != null) {
+      queryParams.addAll(_queryParams('', 'visibility', visibility));
+    }
     if (withPartners != null) {
       queryParams.addAll(_queryParams('', 'withPartners', withPartners));
     }
@@ -113,8 +113,6 @@ class TimelineApi {
   ///
   /// * [String] albumId:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
@@ -129,11 +127,13 @@ class TimelineApi {
   ///
   /// * [String] userId:
   ///
+  /// * [AssetVisibility] visibility:
+  ///
   /// * [bool] withPartners:
   ///
   /// * [bool] withStacked:
-  Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
-    final response = await getTimeBucketWithHttpInfo(size, timeBucket,  albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
+  Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
+    final response = await getTimeBucketWithHttpInfo(size, timeBucket,  albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
@@ -157,8 +157,6 @@ class TimelineApi {
   ///
   /// * [String] albumId:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
@@ -173,10 +171,12 @@ class TimelineApi {
   ///
   /// * [String] userId:
   ///
+  /// * [AssetVisibility] visibility:
+  ///
   /// * [bool] withPartners:
   ///
   /// * [bool] withStacked:
-  Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
+  Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
     // ignore: prefer_const_declarations
     final apiPath = r'/timeline/buckets';
 
@@ -190,9 +190,6 @@ class TimelineApi {
     if (albumId != null) {
       queryParams.addAll(_queryParams('', 'albumId', albumId));
     }
-    if (isArchived != null) {
-      queryParams.addAll(_queryParams('', 'isArchived', isArchived));
-    }
     if (isFavorite != null) {
       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
     }
@@ -215,6 +212,9 @@ class TimelineApi {
     if (userId != null) {
       queryParams.addAll(_queryParams('', 'userId', userId));
     }
+    if (visibility != null) {
+      queryParams.addAll(_queryParams('', 'visibility', visibility));
+    }
     if (withPartners != null) {
       queryParams.addAll(_queryParams('', 'withPartners', withPartners));
     }
@@ -242,8 +242,6 @@ class TimelineApi {
   ///
   /// * [String] albumId:
   ///
-  /// * [bool] isArchived:
-  ///
   /// * [bool] isFavorite:
   ///
   /// * [bool] isTrashed:
@@ -258,11 +256,13 @@ class TimelineApi {
   ///
   /// * [String] userId:
   ///
+  /// * [AssetVisibility] visibility:
+  ///
   /// * [bool] withPartners:
   ///
   /// * [bool] withStacked:
-  Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
-    final response = await getTimeBucketsWithHttpInfo(size,  albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
+  Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
+    final response = await getTimeBucketsWithHttpInfo(size,  albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index ec5eb09729..041a584296 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -268,6 +268,8 @@ class ApiClient {
           return AssetStatsResponseDto.fromJson(value);
         case 'AssetTypeEnum':
           return AssetTypeEnumTypeTransformer().decode(value);
+        case 'AssetVisibility':
+          return AssetVisibilityTypeTransformer().decode(value);
         case 'AudioCodec':
           return AudioCodecTypeTransformer().decode(value);
         case 'AvatarUpdate':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index eec991e903..4928adf767 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -73,6 +73,9 @@ String parameterToString(dynamic value) {
   if (value is AssetTypeEnum) {
     return AssetTypeEnumTypeTransformer().encode(value).toString();
   }
+  if (value is AssetVisibility) {
+    return AssetVisibilityTypeTransformer().encode(value).toString();
+  }
   if (value is AudioCodec) {
     return AudioCodecTypeTransformer().encode(value).toString();
   }
diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
index 0b5a2c30d9..39d7cd996f 100644
--- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart
+++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart
@@ -16,11 +16,11 @@ class AssetBulkUpdateDto {
     this.dateTimeOriginal,
     this.duplicateId,
     this.ids = const [],
-    this.isArchived,
     this.isFavorite,
     this.latitude,
     this.longitude,
     this.rating,
+    this.visibility,
   });
 
   ///
@@ -35,14 +35,6 @@ class AssetBulkUpdateDto {
 
   List<String> ids;
 
-  ///
-  /// 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? isArchived;
-
   ///
   /// 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
@@ -77,16 +69,24 @@ class AssetBulkUpdateDto {
   ///
   num? rating;
 
+  ///
+  /// 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.
+  ///
+  AssetVisibility? visibility;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
     other.dateTimeOriginal == dateTimeOriginal &&
     other.duplicateId == duplicateId &&
     _deepEquality.equals(other.ids, ids) &&
-    other.isArchived == isArchived &&
     other.isFavorite == isFavorite &&
     other.latitude == latitude &&
     other.longitude == longitude &&
-    other.rating == rating;
+    other.rating == rating &&
+    other.visibility == visibility;
 
   @override
   int get hashCode =>
@@ -94,14 +94,14 @@ class AssetBulkUpdateDto {
     (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
     (duplicateId == null ? 0 : duplicateId!.hashCode) +
     (ids.hashCode) +
-    (isArchived == null ? 0 : isArchived!.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (latitude == null ? 0 : latitude!.hashCode) +
     (longitude == null ? 0 : longitude!.hashCode) +
-    (rating == null ? 0 : rating!.hashCode);
+    (rating == null ? 0 : rating!.hashCode) +
+    (visibility == null ? 0 : visibility!.hashCode);
 
   @override
-  String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
+  String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -116,11 +116,6 @@ class AssetBulkUpdateDto {
     //  json[r'duplicateId'] = null;
     }
       json[r'ids'] = this.ids;
-    if (this.isArchived != null) {
-      json[r'isArchived'] = this.isArchived;
-    } else {
-    //  json[r'isArchived'] = null;
-    }
     if (this.isFavorite != null) {
       json[r'isFavorite'] = this.isFavorite;
     } else {
@@ -141,6 +136,11 @@ class AssetBulkUpdateDto {
     } else {
     //  json[r'rating'] = null;
     }
+    if (this.visibility != null) {
+      json[r'visibility'] = this.visibility;
+    } else {
+    //  json[r'visibility'] = null;
+    }
     return json;
   }
 
@@ -158,11 +158,11 @@ class AssetBulkUpdateDto {
         ids: json[r'ids'] is Iterable
             ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
             : const [],
-        isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         latitude: num.parse('${json[r'latitude']}'),
         longitude: num.parse('${json[r'longitude']}'),
         rating: num.parse('${json[r'rating']}'),
+        visibility: AssetVisibility.fromJson(json[r'visibility']),
       );
     }
     return null;
diff --git a/mobile/openapi/lib/model/asset_visibility.dart b/mobile/openapi/lib/model/asset_visibility.dart
new file mode 100644
index 0000000000..4d0c7ee8d3
--- /dev/null
+++ b/mobile/openapi/lib/model/asset_visibility.dart
@@ -0,0 +1,88 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+
+class AssetVisibility {
+  /// Instantiate a new enum with the provided [value].
+  const AssetVisibility._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const archive = AssetVisibility._(r'archive');
+  static const timeline = AssetVisibility._(r'timeline');
+  static const hidden = AssetVisibility._(r'hidden');
+
+  /// List of all possible values in this [enum][AssetVisibility].
+  static const values = <AssetVisibility>[
+    archive,
+    timeline,
+    hidden,
+  ];
+
+  static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value);
+
+  static List<AssetVisibility> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <AssetVisibility>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = AssetVisibility.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [AssetVisibility] to String,
+/// and [decode] dynamic data back to [AssetVisibility].
+class AssetVisibilityTypeTransformer {
+  factory AssetVisibilityTypeTransformer() => _instance ??= const AssetVisibilityTypeTransformer._();
+
+  const AssetVisibilityTypeTransformer._();
+
+  String encode(AssetVisibility data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a AssetVisibility.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  AssetVisibility? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'archive': return AssetVisibility.archive;
+        case r'timeline': return AssetVisibility.timeline;
+        case r'hidden': return AssetVisibility.hidden;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [AssetVisibilityTypeTransformer] instance.
+  static AssetVisibilityTypeTransformer? _instance;
+}
+
diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart
index 3fb003d164..7f1184467b 100644
--- a/mobile/openapi/lib/model/metadata_search_dto.dart
+++ b/mobile/openapi/lib/model/metadata_search_dto.dart
@@ -23,13 +23,11 @@ class MetadataSearchDto {
     this.deviceId,
     this.encodedVideoPath,
     this.id,
-    this.isArchived,
     this.isEncoded,
     this.isFavorite,
     this.isMotion,
     this.isNotInAlbum,
     this.isOffline,
-    this.isVisible,
     this.lensModel,
     this.libraryId,
     this.make,
@@ -52,7 +50,7 @@ class MetadataSearchDto {
     this.type,
     this.updatedAfter,
     this.updatedBefore,
-    this.withArchived = false,
+    this.visibility,
     this.withDeleted,
     this.withExif,
     this.withPeople,
@@ -127,14 +125,6 @@ class MetadataSearchDto {
   ///
   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? isArchived;
-
   ///
   /// 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
@@ -175,14 +165,6 @@ class MetadataSearchDto {
   ///
   bool? isOffline;
 
-  ///
-  /// 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? isVisible;
-
   String? lensModel;
 
   String? libraryId;
@@ -322,7 +304,13 @@ class MetadataSearchDto {
   ///
   DateTime? updatedBefore;
 
-  bool withArchived;
+  ///
+  /// 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.
+  ///
+  AssetVisibility? visibility;
 
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -368,13 +356,11 @@ class MetadataSearchDto {
     other.deviceId == deviceId &&
     other.encodedVideoPath == encodedVideoPath &&
     other.id == id &&
-    other.isArchived == isArchived &&
     other.isEncoded == isEncoded &&
     other.isFavorite == isFavorite &&
     other.isMotion == isMotion &&
     other.isNotInAlbum == isNotInAlbum &&
     other.isOffline == isOffline &&
-    other.isVisible == isVisible &&
     other.lensModel == lensModel &&
     other.libraryId == libraryId &&
     other.make == make &&
@@ -397,7 +383,7 @@ class MetadataSearchDto {
     other.type == type &&
     other.updatedAfter == updatedAfter &&
     other.updatedBefore == updatedBefore &&
-    other.withArchived == withArchived &&
+    other.visibility == visibility &&
     other.withDeleted == withDeleted &&
     other.withExif == withExif &&
     other.withPeople == withPeople &&
@@ -416,13 +402,11 @@ class MetadataSearchDto {
     (deviceId == null ? 0 : deviceId!.hashCode) +
     (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
     (id == null ? 0 : id!.hashCode) +
-    (isArchived == null ? 0 : isArchived!.hashCode) +
     (isEncoded == null ? 0 : isEncoded!.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isMotion == null ? 0 : isMotion!.hashCode) +
     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
     (isOffline == null ? 0 : isOffline!.hashCode) +
-    (isVisible == null ? 0 : isVisible!.hashCode) +
     (lensModel == null ? 0 : lensModel!.hashCode) +
     (libraryId == null ? 0 : libraryId!.hashCode) +
     (make == null ? 0 : make!.hashCode) +
@@ -445,14 +429,14 @@ class MetadataSearchDto {
     (type == null ? 0 : type!.hashCode) +
     (updatedAfter == null ? 0 : updatedAfter!.hashCode) +
     (updatedBefore == null ? 0 : updatedBefore!.hashCode) +
-    (withArchived.hashCode) +
+    (visibility == null ? 0 : visibility!.hashCode) +
     (withDeleted == null ? 0 : withDeleted!.hashCode) +
     (withExif == null ? 0 : withExif!.hashCode) +
     (withPeople == null ? 0 : withPeople!.hashCode) +
     (withStacked == null ? 0 : withStacked!.hashCode);
 
   @override
-  String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
+  String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -506,11 +490,6 @@ class MetadataSearchDto {
     } else {
     //  json[r'id'] = null;
     }
-    if (this.isArchived != null) {
-      json[r'isArchived'] = this.isArchived;
-    } else {
-    //  json[r'isArchived'] = null;
-    }
     if (this.isEncoded != null) {
       json[r'isEncoded'] = this.isEncoded;
     } else {
@@ -536,11 +515,6 @@ class MetadataSearchDto {
     } else {
     //  json[r'isOffline'] = null;
     }
-    if (this.isVisible != null) {
-      json[r'isVisible'] = this.isVisible;
-    } else {
-    //  json[r'isVisible'] = null;
-    }
     if (this.lensModel != null) {
       json[r'lensModel'] = this.lensModel;
     } else {
@@ -639,7 +613,11 @@ class MetadataSearchDto {
     } else {
     //  json[r'updatedBefore'] = null;
     }
-      json[r'withArchived'] = this.withArchived;
+    if (this.visibility != null) {
+      json[r'visibility'] = this.visibility;
+    } else {
+    //  json[r'visibility'] = null;
+    }
     if (this.withDeleted != null) {
       json[r'withDeleted'] = this.withDeleted;
     } else {
@@ -682,13 +660,11 @@ class MetadataSearchDto {
         deviceId: mapValueOfType<String>(json, r'deviceId'),
         encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
         id: mapValueOfType<String>(json, r'id'),
-        isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isMotion: mapValueOfType<bool>(json, r'isMotion'),
         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
         isOffline: mapValueOfType<bool>(json, r'isOffline'),
-        isVisible: mapValueOfType<bool>(json, r'isVisible'),
         lensModel: mapValueOfType<String>(json, r'lensModel'),
         libraryId: mapValueOfType<String>(json, r'libraryId'),
         make: mapValueOfType<String>(json, r'make'),
@@ -715,7 +691,7 @@ class MetadataSearchDto {
         type: AssetTypeEnum.fromJson(json[r'type']),
         updatedAfter: mapDateTime(json, r'updatedAfter', r''),
         updatedBefore: mapDateTime(json, r'updatedBefore', r''),
-        withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
+        visibility: AssetVisibility.fromJson(json[r'visibility']),
         withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
         withExif: mapValueOfType<bool>(json, r'withExif'),
         withPeople: mapValueOfType<bool>(json, r'withPeople'),
diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart
index 10727ec10d..0284212efc 100644
--- a/mobile/openapi/lib/model/random_search_dto.dart
+++ b/mobile/openapi/lib/model/random_search_dto.dart
@@ -18,13 +18,11 @@ class RandomSearchDto {
     this.createdAfter,
     this.createdBefore,
     this.deviceId,
-    this.isArchived,
     this.isEncoded,
     this.isFavorite,
     this.isMotion,
     this.isNotInAlbum,
     this.isOffline,
-    this.isVisible,
     this.lensModel,
     this.libraryId,
     this.make,
@@ -41,7 +39,7 @@ class RandomSearchDto {
     this.type,
     this.updatedAfter,
     this.updatedBefore,
-    this.withArchived = false,
+    this.visibility,
     this.withDeleted,
     this.withExif,
     this.withPeople,
@@ -76,14 +74,6 @@ class RandomSearchDto {
   ///
   String? deviceId;
 
-  ///
-  /// 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? isArchived;
-
   ///
   /// 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
@@ -124,14 +114,6 @@ class RandomSearchDto {
   ///
   bool? isOffline;
 
-  ///
-  /// 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? isVisible;
-
   String? lensModel;
 
   String? libraryId;
@@ -228,7 +210,13 @@ class RandomSearchDto {
   ///
   DateTime? updatedBefore;
 
-  bool withArchived;
+  ///
+  /// 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.
+  ///
+  AssetVisibility? visibility;
 
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -269,13 +257,11 @@ class RandomSearchDto {
     other.createdAfter == createdAfter &&
     other.createdBefore == createdBefore &&
     other.deviceId == deviceId &&
-    other.isArchived == isArchived &&
     other.isEncoded == isEncoded &&
     other.isFavorite == isFavorite &&
     other.isMotion == isMotion &&
     other.isNotInAlbum == isNotInAlbum &&
     other.isOffline == isOffline &&
-    other.isVisible == isVisible &&
     other.lensModel == lensModel &&
     other.libraryId == libraryId &&
     other.make == make &&
@@ -292,7 +278,7 @@ class RandomSearchDto {
     other.type == type &&
     other.updatedAfter == updatedAfter &&
     other.updatedBefore == updatedBefore &&
-    other.withArchived == withArchived &&
+    other.visibility == visibility &&
     other.withDeleted == withDeleted &&
     other.withExif == withExif &&
     other.withPeople == withPeople &&
@@ -306,13 +292,11 @@ class RandomSearchDto {
     (createdAfter == null ? 0 : createdAfter!.hashCode) +
     (createdBefore == null ? 0 : createdBefore!.hashCode) +
     (deviceId == null ? 0 : deviceId!.hashCode) +
-    (isArchived == null ? 0 : isArchived!.hashCode) +
     (isEncoded == null ? 0 : isEncoded!.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isMotion == null ? 0 : isMotion!.hashCode) +
     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
     (isOffline == null ? 0 : isOffline!.hashCode) +
-    (isVisible == null ? 0 : isVisible!.hashCode) +
     (lensModel == null ? 0 : lensModel!.hashCode) +
     (libraryId == null ? 0 : libraryId!.hashCode) +
     (make == null ? 0 : make!.hashCode) +
@@ -329,14 +313,14 @@ class RandomSearchDto {
     (type == null ? 0 : type!.hashCode) +
     (updatedAfter == null ? 0 : updatedAfter!.hashCode) +
     (updatedBefore == null ? 0 : updatedBefore!.hashCode) +
-    (withArchived.hashCode) +
+    (visibility == null ? 0 : visibility!.hashCode) +
     (withDeleted == null ? 0 : withDeleted!.hashCode) +
     (withExif == null ? 0 : withExif!.hashCode) +
     (withPeople == null ? 0 : withPeople!.hashCode) +
     (withStacked == null ? 0 : withStacked!.hashCode);
 
   @override
-  String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
+  String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -365,11 +349,6 @@ class RandomSearchDto {
     } else {
     //  json[r'deviceId'] = null;
     }
-    if (this.isArchived != null) {
-      json[r'isArchived'] = this.isArchived;
-    } else {
-    //  json[r'isArchived'] = null;
-    }
     if (this.isEncoded != null) {
       json[r'isEncoded'] = this.isEncoded;
     } else {
@@ -395,11 +374,6 @@ class RandomSearchDto {
     } else {
     //  json[r'isOffline'] = null;
     }
-    if (this.isVisible != null) {
-      json[r'isVisible'] = this.isVisible;
-    } else {
-    //  json[r'isVisible'] = null;
-    }
     if (this.lensModel != null) {
       json[r'lensModel'] = this.lensModel;
     } else {
@@ -472,7 +446,11 @@ class RandomSearchDto {
     } else {
     //  json[r'updatedBefore'] = null;
     }
-      json[r'withArchived'] = this.withArchived;
+    if (this.visibility != null) {
+      json[r'visibility'] = this.visibility;
+    } else {
+    //  json[r'visibility'] = null;
+    }
     if (this.withDeleted != null) {
       json[r'withDeleted'] = this.withDeleted;
     } else {
@@ -510,13 +488,11 @@ class RandomSearchDto {
         createdAfter: mapDateTime(json, r'createdAfter', r''),
         createdBefore: mapDateTime(json, r'createdBefore', r''),
         deviceId: mapValueOfType<String>(json, r'deviceId'),
-        isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isMotion: mapValueOfType<bool>(json, r'isMotion'),
         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
         isOffline: mapValueOfType<bool>(json, r'isOffline'),
-        isVisible: mapValueOfType<bool>(json, r'isVisible'),
         lensModel: mapValueOfType<String>(json, r'lensModel'),
         libraryId: mapValueOfType<String>(json, r'libraryId'),
         make: mapValueOfType<String>(json, r'make'),
@@ -537,7 +513,7 @@ class RandomSearchDto {
         type: AssetTypeEnum.fromJson(json[r'type']),
         updatedAfter: mapDateTime(json, r'updatedAfter', r''),
         updatedBefore: mapDateTime(json, r'updatedBefore', r''),
-        withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
+        visibility: AssetVisibility.fromJson(json[r'visibility']),
         withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
         withExif: mapValueOfType<bool>(json, r'withExif'),
         withPeople: mapValueOfType<bool>(json, r'withPeople'),
diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart
index 47c800ff09..a915d97b31 100644
--- a/mobile/openapi/lib/model/smart_search_dto.dart
+++ b/mobile/openapi/lib/model/smart_search_dto.dart
@@ -18,13 +18,11 @@ class SmartSearchDto {
     this.createdAfter,
     this.createdBefore,
     this.deviceId,
-    this.isArchived,
     this.isEncoded,
     this.isFavorite,
     this.isMotion,
     this.isNotInAlbum,
     this.isOffline,
-    this.isVisible,
     this.language,
     this.lensModel,
     this.libraryId,
@@ -44,7 +42,7 @@ class SmartSearchDto {
     this.type,
     this.updatedAfter,
     this.updatedBefore,
-    this.withArchived = false,
+    this.visibility,
     this.withDeleted,
     this.withExif,
   });
@@ -77,14 +75,6 @@ class SmartSearchDto {
   ///
   String? deviceId;
 
-  ///
-  /// 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? isArchived;
-
   ///
   /// 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
@@ -125,14 +115,6 @@ class SmartSearchDto {
   ///
   bool? isOffline;
 
-  ///
-  /// 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? isVisible;
-
   ///
   /// 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
@@ -248,7 +230,13 @@ class SmartSearchDto {
   ///
   DateTime? updatedBefore;
 
-  bool withArchived;
+  ///
+  /// 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.
+  ///
+  AssetVisibility? visibility;
 
   ///
   /// Please note: This property should have been non-nullable! Since the specification file
@@ -273,13 +261,11 @@ class SmartSearchDto {
     other.createdAfter == createdAfter &&
     other.createdBefore == createdBefore &&
     other.deviceId == deviceId &&
-    other.isArchived == isArchived &&
     other.isEncoded == isEncoded &&
     other.isFavorite == isFavorite &&
     other.isMotion == isMotion &&
     other.isNotInAlbum == isNotInAlbum &&
     other.isOffline == isOffline &&
-    other.isVisible == isVisible &&
     other.language == language &&
     other.lensModel == lensModel &&
     other.libraryId == libraryId &&
@@ -299,7 +285,7 @@ class SmartSearchDto {
     other.type == type &&
     other.updatedAfter == updatedAfter &&
     other.updatedBefore == updatedBefore &&
-    other.withArchived == withArchived &&
+    other.visibility == visibility &&
     other.withDeleted == withDeleted &&
     other.withExif == withExif;
 
@@ -311,13 +297,11 @@ class SmartSearchDto {
     (createdAfter == null ? 0 : createdAfter!.hashCode) +
     (createdBefore == null ? 0 : createdBefore!.hashCode) +
     (deviceId == null ? 0 : deviceId!.hashCode) +
-    (isArchived == null ? 0 : isArchived!.hashCode) +
     (isEncoded == null ? 0 : isEncoded!.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (isMotion == null ? 0 : isMotion!.hashCode) +
     (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
     (isOffline == null ? 0 : isOffline!.hashCode) +
-    (isVisible == null ? 0 : isVisible!.hashCode) +
     (language == null ? 0 : language!.hashCode) +
     (lensModel == null ? 0 : lensModel!.hashCode) +
     (libraryId == null ? 0 : libraryId!.hashCode) +
@@ -337,12 +321,12 @@ class SmartSearchDto {
     (type == null ? 0 : type!.hashCode) +
     (updatedAfter == null ? 0 : updatedAfter!.hashCode) +
     (updatedBefore == null ? 0 : updatedBefore!.hashCode) +
-    (withArchived.hashCode) +
+    (visibility == null ? 0 : visibility!.hashCode) +
     (withDeleted == null ? 0 : withDeleted!.hashCode) +
     (withExif == null ? 0 : withExif!.hashCode);
 
   @override
-  String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
+  String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -371,11 +355,6 @@ class SmartSearchDto {
     } else {
     //  json[r'deviceId'] = null;
     }
-    if (this.isArchived != null) {
-      json[r'isArchived'] = this.isArchived;
-    } else {
-    //  json[r'isArchived'] = null;
-    }
     if (this.isEncoded != null) {
       json[r'isEncoded'] = this.isEncoded;
     } else {
@@ -401,11 +380,6 @@ class SmartSearchDto {
     } else {
     //  json[r'isOffline'] = null;
     }
-    if (this.isVisible != null) {
-      json[r'isVisible'] = this.isVisible;
-    } else {
-    //  json[r'isVisible'] = null;
-    }
     if (this.language != null) {
       json[r'language'] = this.language;
     } else {
@@ -489,7 +463,11 @@ class SmartSearchDto {
     } else {
     //  json[r'updatedBefore'] = null;
     }
-      json[r'withArchived'] = this.withArchived;
+    if (this.visibility != null) {
+      json[r'visibility'] = this.visibility;
+    } else {
+    //  json[r'visibility'] = null;
+    }
     if (this.withDeleted != null) {
       json[r'withDeleted'] = this.withDeleted;
     } else {
@@ -517,13 +495,11 @@ class SmartSearchDto {
         createdAfter: mapDateTime(json, r'createdAfter', r''),
         createdBefore: mapDateTime(json, r'createdBefore', r''),
         deviceId: mapValueOfType<String>(json, r'deviceId'),
-        isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         isMotion: mapValueOfType<bool>(json, r'isMotion'),
         isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
         isOffline: mapValueOfType<bool>(json, r'isOffline'),
-        isVisible: mapValueOfType<bool>(json, r'isVisible'),
         language: mapValueOfType<String>(json, r'language'),
         lensModel: mapValueOfType<String>(json, r'lensModel'),
         libraryId: mapValueOfType<String>(json, r'libraryId'),
@@ -547,7 +523,7 @@ class SmartSearchDto {
         type: AssetTypeEnum.fromJson(json[r'type']),
         updatedAfter: mapDateTime(json, r'updatedAfter', r''),
         updatedBefore: mapDateTime(json, r'updatedBefore', r''),
-        withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
+        visibility: AssetVisibility.fromJson(json[r'visibility']),
         withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
         withExif: mapValueOfType<bool>(json, r'withExif'),
       );
diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart
index 6f9d7d7eaf..e1d3199428 100644
--- a/mobile/openapi/lib/model/sync_asset_v1.dart
+++ b/mobile/openapi/lib/model/sync_asset_v1.dart
@@ -19,11 +19,11 @@ class SyncAssetV1 {
     required this.fileModifiedAt,
     required this.id,
     required this.isFavorite,
-    required this.isVisible,
     required this.localDateTime,
     required this.ownerId,
     required this.thumbhash,
     required this.type,
+    required this.visibility,
   });
 
   String checksum;
@@ -38,8 +38,6 @@ class SyncAssetV1 {
 
   bool isFavorite;
 
-  bool isVisible;
-
   DateTime? localDateTime;
 
   String ownerId;
@@ -48,6 +46,8 @@ class SyncAssetV1 {
 
   SyncAssetV1TypeEnum type;
 
+  SyncAssetV1VisibilityEnum visibility;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
     other.checksum == checksum &&
@@ -56,11 +56,11 @@ class SyncAssetV1 {
     other.fileModifiedAt == fileModifiedAt &&
     other.id == id &&
     other.isFavorite == isFavorite &&
-    other.isVisible == isVisible &&
     other.localDateTime == localDateTime &&
     other.ownerId == ownerId &&
     other.thumbhash == thumbhash &&
-    other.type == type;
+    other.type == type &&
+    other.visibility == visibility;
 
   @override
   int get hashCode =>
@@ -71,14 +71,14 @@ class SyncAssetV1 {
     (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
     (id.hashCode) +
     (isFavorite.hashCode) +
-    (isVisible.hashCode) +
     (localDateTime == null ? 0 : localDateTime!.hashCode) +
     (ownerId.hashCode) +
     (thumbhash == null ? 0 : thumbhash!.hashCode) +
-    (type.hashCode);
+    (type.hashCode) +
+    (visibility.hashCode);
 
   @override
-  String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, isVisible=$isVisible, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type]';
+  String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -100,7 +100,6 @@ class SyncAssetV1 {
     }
       json[r'id'] = this.id;
       json[r'isFavorite'] = this.isFavorite;
-      json[r'isVisible'] = this.isVisible;
     if (this.localDateTime != null) {
       json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
     } else {
@@ -113,6 +112,7 @@ class SyncAssetV1 {
     //  json[r'thumbhash'] = null;
     }
       json[r'type'] = this.type;
+      json[r'visibility'] = this.visibility;
     return json;
   }
 
@@ -131,11 +131,11 @@ class SyncAssetV1 {
         fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
         id: mapValueOfType<String>(json, r'id')!,
         isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
-        isVisible: mapValueOfType<bool>(json, r'isVisible')!,
         localDateTime: mapDateTime(json, r'localDateTime', r''),
         ownerId: mapValueOfType<String>(json, r'ownerId')!,
         thumbhash: mapValueOfType<String>(json, r'thumbhash'),
         type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!,
+        visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!,
       );
     }
     return null;
@@ -189,11 +189,11 @@ class SyncAssetV1 {
     'fileModifiedAt',
     'id',
     'isFavorite',
-    'isVisible',
     'localDateTime',
     'ownerId',
     'thumbhash',
     'type',
+    'visibility',
   };
 }
 
@@ -277,3 +277,80 @@ class SyncAssetV1TypeEnumTypeTransformer {
 }
 
 
+
+class SyncAssetV1VisibilityEnum {
+  /// Instantiate a new enum with the provided [value].
+  const SyncAssetV1VisibilityEnum._(this.value);
+
+  /// The underlying value of this enum member.
+  final String value;
+
+  @override
+  String toString() => value;
+
+  String toJson() => value;
+
+  static const archive = SyncAssetV1VisibilityEnum._(r'archive');
+  static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
+  static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
+
+  /// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
+  static const values = <SyncAssetV1VisibilityEnum>[
+    archive,
+    timeline,
+    hidden,
+  ];
+
+  static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
+
+  static List<SyncAssetV1VisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <SyncAssetV1VisibilityEnum>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = SyncAssetV1VisibilityEnum.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+}
+
+/// Transformation class that can [encode] an instance of [SyncAssetV1VisibilityEnum] to String,
+/// and [decode] dynamic data back to [SyncAssetV1VisibilityEnum].
+class SyncAssetV1VisibilityEnumTypeTransformer {
+  factory SyncAssetV1VisibilityEnumTypeTransformer() => _instance ??= const SyncAssetV1VisibilityEnumTypeTransformer._();
+
+  const SyncAssetV1VisibilityEnumTypeTransformer._();
+
+  String encode(SyncAssetV1VisibilityEnum data) => data.value;
+
+  /// Decodes a [dynamic value][data] to a SyncAssetV1VisibilityEnum.
+  ///
+  /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+  /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+  /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+  ///
+  /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+  /// and users are still using an old app with the old code.
+  SyncAssetV1VisibilityEnum? decode(dynamic data, {bool allowNull = true}) {
+    if (data != null) {
+      switch (data) {
+        case r'archive': return SyncAssetV1VisibilityEnum.archive;
+        case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
+        case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
+        default:
+          if (!allowNull) {
+            throw ArgumentError('Unknown enum value to decode: $data');
+          }
+      }
+    }
+    return null;
+  }
+
+  /// Singleton [SyncAssetV1VisibilityEnumTypeTransformer] instance.
+  static SyncAssetV1VisibilityEnumTypeTransformer? _instance;
+}
+
+
diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart
index c6ae6d8e07..7b364f1387 100644
--- a/mobile/openapi/lib/model/update_asset_dto.dart
+++ b/mobile/openapi/lib/model/update_asset_dto.dart
@@ -15,12 +15,12 @@ class UpdateAssetDto {
   UpdateAssetDto({
     this.dateTimeOriginal,
     this.description,
-    this.isArchived,
     this.isFavorite,
     this.latitude,
     this.livePhotoVideoId,
     this.longitude,
     this.rating,
+    this.visibility,
   });
 
   ///
@@ -39,14 +39,6 @@ class UpdateAssetDto {
   ///
   String? description;
 
-  ///
-  /// 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? isArchived;
-
   ///
   /// 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
@@ -83,31 +75,39 @@ class UpdateAssetDto {
   ///
   num? rating;
 
+  ///
+  /// 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.
+  ///
+  AssetVisibility? visibility;
+
   @override
   bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
     other.dateTimeOriginal == dateTimeOriginal &&
     other.description == description &&
-    other.isArchived == isArchived &&
     other.isFavorite == isFavorite &&
     other.latitude == latitude &&
     other.livePhotoVideoId == livePhotoVideoId &&
     other.longitude == longitude &&
-    other.rating == rating;
+    other.rating == rating &&
+    other.visibility == visibility;
 
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
     (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
     (description == null ? 0 : description!.hashCode) +
-    (isArchived == null ? 0 : isArchived!.hashCode) +
     (isFavorite == null ? 0 : isFavorite!.hashCode) +
     (latitude == null ? 0 : latitude!.hashCode) +
     (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
     (longitude == null ? 0 : longitude!.hashCode) +
-    (rating == null ? 0 : rating!.hashCode);
+    (rating == null ? 0 : rating!.hashCode) +
+    (visibility == null ? 0 : visibility!.hashCode);
 
   @override
-  String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
+  String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating, visibility=$visibility]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
@@ -121,11 +121,6 @@ class UpdateAssetDto {
     } else {
     //  json[r'description'] = null;
     }
-    if (this.isArchived != null) {
-      json[r'isArchived'] = this.isArchived;
-    } else {
-    //  json[r'isArchived'] = null;
-    }
     if (this.isFavorite != null) {
       json[r'isFavorite'] = this.isFavorite;
     } else {
@@ -151,6 +146,11 @@ class UpdateAssetDto {
     } else {
     //  json[r'rating'] = null;
     }
+    if (this.visibility != null) {
+      json[r'visibility'] = this.visibility;
+    } else {
+    //  json[r'visibility'] = null;
+    }
     return json;
   }
 
@@ -165,12 +165,12 @@ class UpdateAssetDto {
       return UpdateAssetDto(
         dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
         description: mapValueOfType<String>(json, r'description'),
-        isArchived: mapValueOfType<bool>(json, r'isArchived'),
         isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
         latitude: num.parse('${json[r'latitude']}'),
         livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
         longitude: num.parse('${json[r'longitude']}'),
         rating: num.parse('${json[r'rating']}'),
+        visibility: AssetVisibility.fromJson(json[r'visibility']),
       );
     }
     return null;
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 0951177c72..c4c9e7d193 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -1781,14 +1781,6 @@
       "get": {
         "operationId": "getAssetStatistics",
         "parameters": [
-          {
-            "name": "isArchived",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "boolean"
-            }
-          },
           {
             "name": "isFavorite",
             "required": false,
@@ -1804,6 +1796,14 @@
             "schema": {
               "type": "boolean"
             }
+          },
+          {
+            "name": "visibility",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "$ref": "#/components/schemas/AssetVisibility"
+            }
           }
         ],
         "responses": {
@@ -6909,14 +6909,6 @@
               "type": "string"
             }
           },
-          {
-            "name": "isArchived",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "boolean"
-            }
-          },
           {
             "name": "isFavorite",
             "required": false,
@@ -6992,6 +6984,14 @@
               "type": "string"
             }
           },
+          {
+            "name": "visibility",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "$ref": "#/components/schemas/AssetVisibility"
+            }
+          },
           {
             "name": "withPartners",
             "required": false,
@@ -7053,14 +7053,6 @@
               "type": "string"
             }
           },
-          {
-            "name": "isArchived",
-            "required": false,
-            "in": "query",
-            "schema": {
-              "type": "boolean"
-            }
-          },
           {
             "name": "isFavorite",
             "required": false,
@@ -7128,6 +7120,14 @@
               "type": "string"
             }
           },
+          {
+            "name": "visibility",
+            "required": false,
+            "in": "query",
+            "schema": {
+              "$ref": "#/components/schemas/AssetVisibility"
+            }
+          },
           {
             "name": "withPartners",
             "required": false,
@@ -8273,9 +8273,6 @@
             },
             "type": "array"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isFavorite": {
             "type": "boolean"
           },
@@ -8289,6 +8286,13 @@
             "maximum": 5,
             "minimum": -1,
             "type": "number"
+          },
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           }
         },
         "required": [
@@ -8713,15 +8717,9 @@
             "format": "date-time",
             "type": "string"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isFavorite": {
             "type": "boolean"
           },
-          "isVisible": {
-            "type": "boolean"
-          },
           "livePhotoVideoId": {
             "format": "uuid",
             "type": "string"
@@ -8729,6 +8727,13 @@
           "sidecarData": {
             "format": "binary",
             "type": "string"
+          },
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           }
         },
         "required": [
@@ -9009,6 +9014,14 @@
         ],
         "type": "string"
       },
+      "AssetVisibility": {
+        "enum": [
+          "archive",
+          "timeline",
+          "hidden"
+        ],
+        "type": "string"
+      },
       "AudioCodec": {
         "enum": [
           "mp3",
@@ -10204,9 +10217,6 @@
             "format": "uuid",
             "type": "string"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isEncoded": {
             "type": "boolean"
           },
@@ -10222,9 +10232,6 @@
           "isOffline": {
             "type": "boolean"
           },
-          "isVisible": {
-            "type": "boolean"
-          },
           "lensModel": {
             "nullable": true,
             "type": "string"
@@ -10324,9 +10331,12 @@
             "format": "date-time",
             "type": "string"
           },
-          "withArchived": {
-            "default": false,
-            "type": "boolean"
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           },
           "withDeleted": {
             "type": "boolean"
@@ -11041,9 +11051,6 @@
           "deviceId": {
             "type": "string"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isEncoded": {
             "type": "boolean"
           },
@@ -11059,9 +11066,6 @@
           "isOffline": {
             "type": "boolean"
           },
-          "isVisible": {
-            "type": "boolean"
-          },
           "lensModel": {
             "nullable": true,
             "type": "string"
@@ -11137,9 +11141,12 @@
             "format": "date-time",
             "type": "string"
           },
-          "withArchived": {
-            "default": false,
-            "type": "boolean"
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           },
           "withDeleted": {
             "type": "boolean"
@@ -11989,9 +11996,6 @@
           "deviceId": {
             "type": "string"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isEncoded": {
             "type": "boolean"
           },
@@ -12007,9 +12011,6 @@
           "isOffline": {
             "type": "boolean"
           },
-          "isVisible": {
-            "type": "boolean"
-          },
           "language": {
             "type": "string"
           },
@@ -12095,9 +12096,12 @@
             "format": "date-time",
             "type": "string"
           },
-          "withArchived": {
-            "default": false,
-            "type": "boolean"
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           },
           "withDeleted": {
             "type": "boolean"
@@ -12381,9 +12385,6 @@
           "isFavorite": {
             "type": "boolean"
           },
-          "isVisible": {
-            "type": "boolean"
-          },
           "localDateTime": {
             "format": "date-time",
             "nullable": true,
@@ -12404,6 +12405,14 @@
               "OTHER"
             ],
             "type": "string"
+          },
+          "visibility": {
+            "enum": [
+              "archive",
+              "timeline",
+              "hidden"
+            ],
+            "type": "string"
           }
         },
         "required": [
@@ -12413,11 +12422,11 @@
           "fileModifiedAt",
           "id",
           "isFavorite",
-          "isVisible",
           "localDateTime",
           "ownerId",
           "thumbhash",
-          "type"
+          "type",
+          "visibility"
         ],
         "type": "object"
       },
@@ -13671,9 +13680,6 @@
           "description": {
             "type": "string"
           },
-          "isArchived": {
-            "type": "boolean"
-          },
           "isFavorite": {
             "type": "boolean"
           },
@@ -13692,6 +13698,13 @@
             "maximum": 5,
             "minimum": -1,
             "type": "number"
+          },
+          "visibility": {
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/AssetVisibility"
+              }
+            ]
           }
         },
         "type": "object"
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 20fb72b486..b2abdb0a24 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -413,11 +413,10 @@ export type AssetMediaCreateDto = {
     duration?: string;
     fileCreatedAt: string;
     fileModifiedAt: string;
-    isArchived?: boolean;
     isFavorite?: boolean;
-    isVisible?: boolean;
     livePhotoVideoId?: string;
     sidecarData?: Blob;
+    visibility?: AssetVisibility;
 };
 export type AssetMediaResponseDto = {
     id: string;
@@ -427,11 +426,11 @@ export type AssetBulkUpdateDto = {
     dateTimeOriginal?: string;
     duplicateId?: string | null;
     ids: string[];
-    isArchived?: boolean;
     isFavorite?: boolean;
     latitude?: number;
     longitude?: number;
     rating?: number;
+    visibility?: AssetVisibility;
 };
 export type AssetBulkUploadCheckItem = {
     /** base64 or hex encoded sha1 hash */
@@ -470,12 +469,12 @@ export type AssetStatsResponseDto = {
 export type UpdateAssetDto = {
     dateTimeOriginal?: string;
     description?: string;
-    isArchived?: boolean;
     isFavorite?: boolean;
     latitude?: number;
     livePhotoVideoId?: string | null;
     longitude?: number;
     rating?: number;
+    visibility?: AssetVisibility;
 };
 export type AssetMediaReplaceDto = {
     assetData: Blob;
@@ -815,13 +814,11 @@ export type MetadataSearchDto = {
     deviceId?: string;
     encodedVideoPath?: string;
     id?: string;
-    isArchived?: boolean;
     isEncoded?: boolean;
     isFavorite?: boolean;
     isMotion?: boolean;
     isNotInAlbum?: boolean;
     isOffline?: boolean;
-    isVisible?: boolean;
     lensModel?: string | null;
     libraryId?: string | null;
     make?: string;
@@ -844,7 +841,7 @@ export type MetadataSearchDto = {
     "type"?: AssetTypeEnum;
     updatedAfter?: string;
     updatedBefore?: string;
-    withArchived?: boolean;
+    visibility?: AssetVisibility;
     withDeleted?: boolean;
     withExif?: boolean;
     withPeople?: boolean;
@@ -888,13 +885,11 @@ export type RandomSearchDto = {
     createdAfter?: string;
     createdBefore?: string;
     deviceId?: string;
-    isArchived?: boolean;
     isEncoded?: boolean;
     isFavorite?: boolean;
     isMotion?: boolean;
     isNotInAlbum?: boolean;
     isOffline?: boolean;
-    isVisible?: boolean;
     lensModel?: string | null;
     libraryId?: string | null;
     make?: string;
@@ -911,7 +906,7 @@ export type RandomSearchDto = {
     "type"?: AssetTypeEnum;
     updatedAfter?: string;
     updatedBefore?: string;
-    withArchived?: boolean;
+    visibility?: AssetVisibility;
     withDeleted?: boolean;
     withExif?: boolean;
     withPeople?: boolean;
@@ -923,13 +918,11 @@ export type SmartSearchDto = {
     createdAfter?: string;
     createdBefore?: string;
     deviceId?: string;
-    isArchived?: boolean;
     isEncoded?: boolean;
     isFavorite?: boolean;
     isMotion?: boolean;
     isNotInAlbum?: boolean;
     isOffline?: boolean;
-    isVisible?: boolean;
     language?: string;
     lensModel?: string | null;
     libraryId?: string | null;
@@ -949,7 +942,7 @@ export type SmartSearchDto = {
     "type"?: AssetTypeEnum;
     updatedAfter?: string;
     updatedBefore?: string;
-    withArchived?: boolean;
+    visibility?: AssetVisibility;
     withDeleted?: boolean;
     withExif?: boolean;
 };
@@ -1877,18 +1870,18 @@ export function getRandom({ count }: {
         ...opts
     }));
 }
-export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: {
-    isArchived?: boolean;
+export function getAssetStatistics({ isFavorite, isTrashed, visibility }: {
     isFavorite?: boolean;
     isTrashed?: boolean;
+    visibility?: AssetVisibility;
 }, opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
         data: AssetStatsResponseDto;
     }>(`/assets/statistics${QS.query(QS.explode({
-        isArchived,
         isFavorite,
-        isTrashed
+        isTrashed,
+        visibility
     }))}`, {
         ...opts
     }));
@@ -3242,9 +3235,8 @@ export function tagAssets({ id, bulkIdsDto }: {
         body: bulkIdsDto
     })));
 }
-export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: {
+export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
     albumId?: string;
-    isArchived?: boolean;
     isFavorite?: boolean;
     isTrashed?: boolean;
     key?: string;
@@ -3254,6 +3246,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
     tagId?: string;
     timeBucket: string;
     userId?: string;
+    visibility?: AssetVisibility;
     withPartners?: boolean;
     withStacked?: boolean;
 }, opts?: Oazapfts.RequestOpts) {
@@ -3262,7 +3255,6 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
         data: AssetResponseDto[];
     }>(`/timeline/bucket${QS.query(QS.explode({
         albumId,
-        isArchived,
         isFavorite,
         isTrashed,
         key,
@@ -3272,15 +3264,15 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
         tagId,
         timeBucket,
         userId,
+        visibility,
         withPartners,
         withStacked
     }))}`, {
         ...opts
     }));
 }
-export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: {
+export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, userId, visibility, withPartners, withStacked }: {
     albumId?: string;
-    isArchived?: boolean;
     isFavorite?: boolean;
     isTrashed?: boolean;
     key?: string;
@@ -3289,6 +3281,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
     size: TimeBucketSize;
     tagId?: string;
     userId?: string;
+    visibility?: AssetVisibility;
     withPartners?: boolean;
     withStacked?: boolean;
 }, opts?: Oazapfts.RequestOpts) {
@@ -3297,7 +3290,6 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
         data: TimeBucketResponseDto[];
     }>(`/timeline/buckets${QS.query(QS.explode({
         albumId,
-        isArchived,
         isFavorite,
         isTrashed,
         key,
@@ -3306,6 +3298,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
         size,
         tagId,
         userId,
+        visibility,
         withPartners,
         withStacked
     }))}`, {
@@ -3620,6 +3613,11 @@ export enum Permission {
     AdminUserUpdate = "admin.user.update",
     AdminUserDelete = "admin.user.delete"
 }
+export enum AssetVisibility {
+    Archive = "archive",
+    Timeline = "timeline",
+    Hidden = "hidden"
+}
 export enum AssetMediaStatus {
     Created = "created",
     Replaced = "replaced",
diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts
index c674dc1f2c..67bdeff222 100644
--- a/server/src/controllers/asset-media.controller.spec.ts
+++ b/server/src/controllers/asset-media.controller.spec.ts
@@ -100,38 +100,29 @@ describe(AssetMediaController.name, () => {
       expect(body).toEqual(factory.responses.badRequest());
     });
 
-    it('should throw if `isVisible` is not a boolean', async () => {
+    it('should throw if `visibility` is not an enum', async () => {
       const { status, body } = await request(ctx.getHttpServer())
         .post('/assets')
         .attach('assetData', assetData, filename)
-        .field({ ...makeUploadDto(), isVisible: 'not-a-boolean' });
+        .field({ ...makeUploadDto(), visibility: 'not-a-boolean' });
       expect(status).toBe(400);
       expect(body).toEqual(factory.responses.badRequest());
     });
 
-    it('should throw if `isArchived` is not a boolean', async () => {
-      const { status, body } = await request(ctx.getHttpServer())
-        .post('/assets')
-        .attach('assetData', assetData, filename)
-        .field({ ...makeUploadDto(), isArchived: 'not-a-boolean' });
-      expect(status).toBe(400);
-      expect(body).toEqual(factory.responses.badRequest());
+    // TODO figure out how to deal with `sendFile`
+    describe.skip('GET /assets/:id/original', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
     });
-  });
 
-  // TODO figure out how to deal with `sendFile`
-  describe.skip('GET /assets/:id/original', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
-      expect(ctx.authenticate).toHaveBeenCalled();
-    });
-  });
-
-  // TODO figure out how to deal with `sendFile`
-  describe.skip('GET /assets/:id/thumbnail', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
-      expect(ctx.authenticate).toHaveBeenCalled();
+    // TODO figure out how to deal with `sendFile`
+    describe.skip('GET /assets/:id/thumbnail', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
     });
   });
 });
diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts
index 03c9625e16..14130fabcb 100644
--- a/server/src/controllers/search.controller.spec.ts
+++ b/server/src/controllers/search.controller.spec.ts
@@ -60,12 +60,14 @@ describe(SearchController.name, () => {
       expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number']));
     });
 
-    it('should reject an isArchived as not a boolean', async () => {
+    it('should reject an visibility as not an enum', async () => {
       const { status, body } = await request(ctx.getHttpServer())
         .post('/search/metadata')
-        .send({ isArchived: 'immich' });
+        .send({ visibility: 'immich' });
       expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value']));
+      expect(body).toEqual(
+        errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden']),
+      );
     });
 
     it('should reject an isFavorite as not a boolean', async () => {
@@ -98,104 +100,98 @@ describe(SearchController.name, () => {
       expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value']));
     });
 
-    it('should reject an isVisible as not a boolean', async () => {
-      const { status, body } = await request(ctx.getHttpServer())
-        .post('/search/metadata')
-        .send({ isVisible: 'immich' });
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value']));
-    });
-  });
+    describe('POST /search/random', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).post('/search/random');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
 
-  describe('POST /search/random', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).post('/search/random');
-      expect(ctx.authenticate).toHaveBeenCalled();
+      it('should reject if withStacked is not a boolean', async () => {
+        const { status, body } = await request(ctx.getHttpServer())
+          .post('/search/random')
+          .send({ withStacked: 'immich' });
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
+      });
+
+      it('should reject if withPeople is not a boolean', async () => {
+        const { status, body } = await request(ctx.getHttpServer())
+          .post('/search/random')
+          .send({ withPeople: 'immich' });
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value']));
+      });
     });
 
-    it('should reject if withStacked is not a boolean', async () => {
-      const { status, body } = await request(ctx.getHttpServer())
-        .post('/search/random')
-        .send({ withStacked: 'immich' });
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
+    describe('POST /search/smart', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).post('/search/smart');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
+
+      it('should require a query', async () => {
+        const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
+      });
     });
 
-    it('should reject if withPeople is not a boolean', async () => {
-      const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' });
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value']));
-    });
-  });
-
-  describe('POST /search/smart', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).post('/search/smart');
-      expect(ctx.authenticate).toHaveBeenCalled();
+    describe('GET /search/explore', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get('/search/explore');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
     });
 
-    it('should require a query', async () => {
-      const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
-    });
-  });
+    describe('POST /search/person', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get('/search/person');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
 
-  describe('GET /search/explore', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get('/search/explore');
-      expect(ctx.authenticate).toHaveBeenCalled();
-    });
-  });
-
-  describe('POST /search/person', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get('/search/person');
-      expect(ctx.authenticate).toHaveBeenCalled();
+      it('should require a name', async () => {
+        const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
+      });
     });
 
-    it('should require a name', async () => {
-      const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
-    });
-  });
+    describe('GET /search/places', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get('/search/places');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
 
-  describe('GET /search/places', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get('/search/places');
-      expect(ctx.authenticate).toHaveBeenCalled();
+      it('should require a name', async () => {
+        const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
+      });
     });
 
-    it('should require a name', async () => {
-      const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
-      expect(status).toBe(400);
-      expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
-    });
-  });
-
-  describe('GET /search/cities', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get('/search/cities');
-      expect(ctx.authenticate).toHaveBeenCalled();
-    });
-  });
-
-  describe('GET /search/suggestions', () => {
-    it('should be an authenticated route', async () => {
-      await request(ctx.getHttpServer()).get('/search/suggestions');
-      expect(ctx.authenticate).toHaveBeenCalled();
+    describe('GET /search/cities', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get('/search/cities');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
     });
 
-    it('should require a type', async () => {
-      const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
-      expect(status).toBe(400);
-      expect(body).toEqual(
-        errorDto.badRequest([
-          'type should not be empty',
-          expect.stringContaining('type must be one of the following values:'),
-        ]),
-      );
+    describe('GET /search/suggestions', () => {
+      it('should be an authenticated route', async () => {
+        await request(ctx.getHttpServer()).get('/search/suggestions');
+        expect(ctx.authenticate).toHaveBeenCalled();
+      });
+
+      it('should require a type', async () => {
+        const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
+        expect(status).toBe(400);
+        expect(body).toEqual(
+          errorDto.badRequest([
+            'type should not be empty',
+            expect.stringContaining('type must be one of the following values:'),
+          ]),
+        );
+      });
     });
   });
 });
diff --git a/server/src/database.ts b/server/src/database.ts
index a93873ef42..a13b074448 100644
--- a/server/src/database.ts
+++ b/server/src/database.ts
@@ -5,6 +5,7 @@ import {
   AlbumUserRole,
   AssetFileType,
   AssetType,
+  AssetVisibility,
   MemoryType,
   Permission,
   SharedLinkType,
@@ -108,7 +109,7 @@ export type Asset = {
   fileCreatedAt: Date;
   fileModifiedAt: Date;
   isExternal: boolean;
-  isVisible: boolean;
+  visibility: AssetVisibility;
   libraryId: string | null;
   livePhotoVideoId: string | null;
   localDateTime: Date;
@@ -285,7 +286,7 @@ export const columns = {
     'assets.fileCreatedAt',
     'assets.fileModifiedAt',
     'assets.isExternal',
-    'assets.isVisible',
+    'assets.visibility',
     'assets.libraryId',
     'assets.livePhotoVideoId',
     'assets.localDateTime',
@@ -345,7 +346,7 @@ export const columns = {
     'type',
     'deletedAt',
     'isFavorite',
-    'isVisible',
+    'visibility',
     'updateId',
   ],
   stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
diff --git a/server/src/db.d.ts b/server/src/db.d.ts
index 85be9d5208..1b039f9982 100644
--- a/server/src/db.d.ts
+++ b/server/src/db.d.ts
@@ -10,6 +10,7 @@ import {
   AssetOrder,
   AssetStatus,
   AssetType,
+  AssetVisibility,
   MemoryType,
   NotificationLevel,
   NotificationType,
@@ -148,11 +149,10 @@ export interface Assets {
   fileCreatedAt: Timestamp;
   fileModifiedAt: Timestamp;
   id: Generated<string>;
-  isArchived: Generated<boolean>;
   isExternal: Generated<boolean>;
   isFavorite: Generated<boolean>;
   isOffline: Generated<boolean>;
-  isVisible: Generated<boolean>;
+  visibility: Generated<AssetVisibility>;
   libraryId: string | null;
   livePhotoVideoId: string | null;
   localDateTime: Timestamp;
diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts
index 8837138599..a647b4515f 100644
--- a/server/src/dtos/asset-media.dto.ts
+++ b/server/src/dtos/asset-media.dto.ts
@@ -1,7 +1,8 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
 import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
-import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
+import { AssetVisibility } from 'src/enum';
+import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 
 export enum AssetMediaSize {
   /**
@@ -55,11 +56,8 @@ export class AssetMediaCreateDto extends AssetMediaBase {
   @ValidateBoolean({ optional: true })
   isFavorite?: boolean;
 
-  @ValidateBoolean({ optional: true })
-  isArchived?: boolean;
-
-  @ValidateBoolean({ optional: true })
-  isVisible?: boolean;
+  @ValidateAssetVisibility({ optional: true })
+  visibility?: AssetVisibility;
 
   @ValidateUUID({ optional: true })
   livePhotoVideoId?: string;
diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts
index 3732e665cd..480ad0b9b9 100644
--- a/server/src/dtos/asset-response.dto.ts
+++ b/server/src/dtos/asset-response.dto.ts
@@ -12,7 +12,7 @@ import {
 } from 'src/dtos/person.dto';
 import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
 import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
-import { AssetStatus, AssetType } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
 import { mimeTypes } from 'src/utils/mime-types';
 
 export class SanitizedAssetResponseDto {
@@ -74,11 +74,10 @@ export type MapAsset = {
   fileCreatedAt: Date;
   fileModifiedAt: Date;
   files?: AssetFile[];
-  isArchived: boolean;
   isExternal: boolean;
   isFavorite: boolean;
   isOffline: boolean;
-  isVisible: boolean;
+  visibility: AssetVisibility;
   libraryId: string | null;
   livePhotoVideoId: string | null;
   localDateTime: Date;
@@ -183,7 +182,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
     localDateTime: entity.localDateTime,
     updatedAt: entity.updatedAt,
     isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
-    isArchived: entity.isArchived,
+    isArchived: entity.visibility === AssetVisibility.ARCHIVE,
     isTrashed: !!entity.deletedAt,
     duration: entity.duration ?? '0:00:00.00000',
     exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts
index 32b14055d5..0789633878 100644
--- a/server/src/dtos/asset.dto.ts
+++ b/server/src/dtos/asset.dto.ts
@@ -14,9 +14,9 @@ import {
   ValidateIf,
 } from 'class-validator';
 import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
-import { AssetType } from 'src/enum';
+import { AssetType, AssetVisibility } from 'src/enum';
 import { AssetStats } from 'src/repositories/asset.repository';
-import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
+import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
 
 export class DeviceIdDto {
   @IsNotEmpty()
@@ -32,8 +32,8 @@ export class UpdateAssetBase {
   @ValidateBoolean({ optional: true })
   isFavorite?: boolean;
 
-  @ValidateBoolean({ optional: true })
-  isArchived?: boolean;
+  @ValidateAssetVisibility({ optional: true })
+  visibility?: AssetVisibility;
 
   @Optional()
   @IsDateString()
@@ -105,8 +105,8 @@ export class AssetJobsDto extends AssetIdsDto {
 }
 
 export class AssetStatsDto {
-  @ValidateBoolean({ optional: true })
-  isArchived?: boolean;
+  @ValidateAssetVisibility({ optional: true })
+  visibility?: AssetVisibility;
 
   @ValidateBoolean({ optional: true })
   isFavorite?: boolean;
diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts
index a7633dce78..579cba680e 100644
--- a/server/src/dtos/search.dto.ts
+++ b/server/src/dtos/search.dto.ts
@@ -5,8 +5,8 @@ import { Place } from 'src/database';
 import { PropertyLifecycle } from 'src/decorators';
 import { AlbumResponseDto } from 'src/dtos/album.dto';
 import { AssetResponseDto } from 'src/dtos/asset-response.dto';
-import { AssetOrder, AssetType } from 'src/enum';
-import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
+import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
+import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 
 class BaseSearchDto {
   @ValidateUUID({ optional: true, nullable: true })
@@ -22,13 +22,6 @@ class BaseSearchDto {
   @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
   type?: AssetType;
 
-  @ValidateBoolean({ optional: true })
-  isArchived?: boolean;
-
-  @ValidateBoolean({ optional: true })
-  @ApiProperty({ default: false })
-  withArchived?: boolean;
-
   @ValidateBoolean({ optional: true })
   isEncoded?: boolean;
 
@@ -41,8 +34,8 @@ class BaseSearchDto {
   @ValidateBoolean({ optional: true })
   isOffline?: boolean;
 
-  @ValidateBoolean({ optional: true })
-  isVisible?: boolean;
+  @ValidateAssetVisibility({ optional: true })
+  visibility?: AssetVisibility;
 
   @ValidateBoolean({ optional: true })
   withDeleted?: boolean;
diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts
index a035f8ecb9..cc11c3410b 100644
--- a/server/src/dtos/sync.dto.ts
+++ b/server/src/dtos/sync.dto.ts
@@ -1,7 +1,7 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
 import { AssetResponseDto } from 'src/dtos/asset-response.dto';
-import { AssetType, SyncEntityType, SyncRequestType } from 'src/enum';
+import { AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum';
 import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
 
 export class AssetFullSyncDto {
@@ -67,7 +67,7 @@ export class SyncAssetV1 {
   type!: AssetType;
   deletedAt!: Date | null;
   isFavorite!: boolean;
-  isVisible!: boolean;
+  visibility!: AssetVisibility;
 }
 
 export class SyncAssetDeleteV1 {
diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts
index a9dfa49a07..51d46871ae 100644
--- a/server/src/dtos/time-bucket.dto.ts
+++ b/server/src/dtos/time-bucket.dto.ts
@@ -1,8 +1,8 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
-import { AssetOrder } from 'src/enum';
+import { AssetOrder, AssetVisibility } from 'src/enum';
 import { TimeBucketSize } from 'src/repositories/asset.repository';
-import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
+import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
 
 export class TimeBucketDto {
   @IsNotEmpty()
@@ -22,9 +22,6 @@ export class TimeBucketDto {
   @ValidateUUID({ optional: true })
   tagId?: string;
 
-  @ValidateBoolean({ optional: true })
-  isArchived?: boolean;
-
   @ValidateBoolean({ optional: true })
   isFavorite?: boolean;
 
@@ -41,6 +38,9 @@ export class TimeBucketDto {
   @Optional()
   @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
   order?: AssetOrder;
+
+  @ValidateAssetVisibility({ optional: true })
+  visibility?: AssetVisibility;
 }
 
 export class TimeBucketAssetDto extends TimeBucketDto {
diff --git a/server/src/enum.ts b/server/src/enum.ts
index a9ea285c24..f214593975 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -618,3 +618,13 @@ export enum DatabaseSslMode {
   Require = 'require',
   VerifyFull = 'verify-full',
 }
+
+export enum AssetVisibility {
+  ARCHIVE = 'archive',
+  TIMELINE = 'timeline',
+
+  /**
+   * Video part of the LivePhotos and MotionPhotos
+   */
+  HIDDEN = 'hidden',
+}
diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql
index 03f1af3b28..f550c5b0c1 100644
--- a/server/src/queries/access.repository.sql
+++ b/server/src/queries/access.repository.sql
@@ -110,8 +110,11 @@ from
   and "assets"."deletedAt" is null
 where
   "partner"."sharedWithId" = $1
-  and "assets"."isArchived" = $2
-  and "assets"."id" in ($3)
+  and (
+    "assets"."visibility" = 'timeline'
+    or "assets"."visibility" = 'hidden'
+  )
+  and "assets"."id" in ($2)
 
 -- AccessRepository.asset.checkSharedLinkAccess
 select
diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql
index d8e8430be7..577635a912 100644
--- a/server/src/queries/asset.job.repository.sql
+++ b/server/src/queries/asset.job.repository.sql
@@ -7,7 +7,7 @@ select
   "ownerId",
   "duplicateId",
   "stackId",
-  "isVisible",
+  "visibility",
   "smart_search"."embedding",
   (
     select
@@ -83,7 +83,7 @@ from
   inner join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id"
 where
   "assets"."deletedAt" is null
-  and "assets"."isVisible" = $1
+  and "assets"."visibility" != $1
   and (
     "asset_job_status"."previewAt" is null
     or "asset_job_status"."thumbnailAt" is null
@@ -118,7 +118,7 @@ where
 -- AssetJobRepository.getForGenerateThumbnailJob
 select
   "assets"."id",
-  "assets"."isVisible",
+  "assets"."visibility",
   "assets"."originalFileName",
   "assets"."originalPath",
   "assets"."ownerId",
@@ -155,7 +155,7 @@ select
   "assets"."fileCreatedAt",
   "assets"."fileModifiedAt",
   "assets"."isExternal",
-  "assets"."isVisible",
+  "assets"."visibility",
   "assets"."libraryId",
   "assets"."livePhotoVideoId",
   "assets"."localDateTime",
@@ -201,7 +201,7 @@ from
   "assets"
   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
 where
-  "assets"."isVisible" = $1
+  "assets"."visibility" != $1
   and "assets"."deletedAt" is null
   and "job_status"."previewAt" is not null
   and not exists (
@@ -220,7 +220,7 @@ from
   "assets"
   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
 where
-  "assets"."isVisible" = $1
+  "assets"."visibility" != $1
   and "assets"."deletedAt" is null
   and "job_status"."previewAt" is not null
   and not exists (
@@ -234,7 +234,7 @@ where
 -- AssetJobRepository.getForClipEncoding
 select
   "assets"."id",
-  "assets"."isVisible",
+  "assets"."visibility",
   (
     select
       coalesce(json_agg(agg), '[]')
@@ -259,7 +259,7 @@ where
 -- AssetJobRepository.getForDetectFacesJob
 select
   "assets"."id",
-  "assets"."isVisible",
+  "assets"."visibility",
   to_json("exif") as "exifInfo",
   (
     select
@@ -312,7 +312,7 @@ where
 -- AssetJobRepository.getForAssetDeletion
 select
   "assets"."id",
-  "assets"."isVisible",
+  "assets"."visibility",
   "assets"."libraryId",
   "assets"."ownerId",
   "assets"."livePhotoVideoId",
@@ -372,7 +372,7 @@ from
       "assets" as "stacked"
     where
       "stacked"."deletedAt" is not null
-      and "stacked"."isArchived" = $1
+      and "stacked"."visibility" != $1
       and "stacked"."stackId" = "asset_stack"."id"
     group by
       "asset_stack"."id"
@@ -391,7 +391,7 @@ where
     "assets"."encodedVideoPath" is null
     or "assets"."encodedVideoPath" = $2
   )
-  and "assets"."isVisible" = $3
+  and "assets"."visibility" != $3
   and "assets"."deletedAt" is null
 
 -- AssetJobRepository.getForVideoConversion
@@ -417,7 +417,7 @@ where
     "asset_job_status"."metadataExtractedAt" is null
     or "asset_job_status"."assetId" is null
   )
-  and "assets"."isVisible" = $1
+  and "assets"."visibility" != $1
   and "assets"."deletedAt" is null
 
 -- AssetJobRepository.getForStorageTemplateJob
@@ -480,7 +480,7 @@ where
     "assets"."sidecarPath" = $1
     or "assets"."sidecarPath" is null
   )
-  and "assets"."isVisible" = $2
+  and "assets"."visibility" != $2
 
 -- AssetJobRepository.streamForDetectFacesJob
 select
@@ -489,7 +489,7 @@ from
   "assets"
   inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
 where
-  "assets"."isVisible" = $1
+  "assets"."visibility" != $1
   and "assets"."deletedAt" is null
   and "job_status"."previewAt" is not null
   and "job_status"."facesRecognizedAt" is null
diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql
index cb438e1c6d..4a3fbf0e39 100644
--- a/server/src/queries/asset.repository.sql
+++ b/server/src/queries/asset.repository.sql
@@ -43,21 +43,20 @@ with
           "asset_job_status"."previewAt" is not null
           and (assets."localDateTime" at time zone 'UTC')::date = today.date
           and "assets"."ownerId" = any ($3::uuid[])
-          and "assets"."isVisible" = $4
-          and "assets"."isArchived" = $5
+          and "assets"."visibility" = $4
           and exists (
             select
             from
               "asset_files"
             where
               "assetId" = "assets"."id"
-              and "asset_files"."type" = $6
+              and "asset_files"."type" = $5
           )
           and "assets"."deletedAt" is null
         order by
           (assets."localDateTime" at time zone 'UTC')::date desc
         limit
-          $7
+          $6
       ) as "a" on true
       inner join "exif" on "a"."id" = "exif"."assetId"
   )
@@ -159,7 +158,7 @@ from
 where
   "ownerId" = $1::uuid
   and "deviceId" = $2
-  and "isVisible" = $3
+  and "visibility" != $3
   and "deletedAt" is null
 
 -- AssetRepository.getLivePhotoCount
@@ -241,7 +240,10 @@ with
       "assets"
     where
       "assets"."deletedAt" is null
-      and "assets"."isVisible" = $2
+      and (
+        "assets"."visibility" = $2
+        or "assets"."visibility" = $3
+      )
   )
 select
   "timeBucket",
@@ -271,7 +273,7 @@ from
     where
       "stacked"."stackId" = "asset_stack"."id"
       and "stacked"."deletedAt" is null
-      and "stacked"."isArchived" = $1
+      and "stacked"."visibility" != $1
     group by
       "asset_stack"."id"
   ) as "stacked_assets" on "asset_stack"."id" is not null
@@ -281,8 +283,11 @@ where
     or "assets"."stackId" is null
   )
   and "assets"."deletedAt" is null
-  and "assets"."isVisible" = $2
-  and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
+  and (
+    "assets"."visibility" = $2
+    or "assets"."visibility" = $3
+  )
+  and date_trunc($4, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $5
 order by
   "assets"."localDateTime" desc
 
@@ -307,7 +312,7 @@ with
       "assets"."ownerId" = $1::uuid
       and "assets"."duplicateId" is not null
       and "assets"."deletedAt" is null
-      and "assets"."isVisible" = $2
+      and "assets"."visibility" != $2
       and "assets"."stackId" is null
     group by
       "assets"."duplicateId"
@@ -365,12 +370,11 @@ from
   inner join "cities" on "exif"."city" = "cities"."city"
 where
   "ownerId" = $2::uuid
-  and "isVisible" = $3
-  and "isArchived" = $4
-  and "type" = $5
+  and "visibility" = $3
+  and "type" = $4
   and "deletedAt" is null
 limit
-  $6
+  $5
 
 -- AssetRepository.getAllForUserFullSync
 select
@@ -394,7 +398,7 @@ from
   ) as "stacked_assets" on "asset_stack"."id" is not null
 where
   "assets"."ownerId" = $1::uuid
-  and "assets"."isVisible" = $2
+  and "assets"."visibility" != $2
   and "assets"."updatedAt" <= $3
   and "assets"."id" > $4
 order by
@@ -424,7 +428,7 @@ from
   ) as "stacked_assets" on "asset_stack"."id" is not null
 where
   "assets"."ownerId" = any ($1::uuid[])
-  and "assets"."isVisible" = $2
+  and "assets"."visibility" != $2
   and "assets"."updatedAt" > $3
 limit
   $4
diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql
index 43500a8748..f17d9663d6 100644
--- a/server/src/queries/library.repository.sql
+++ b/server/src/queries/library.repository.sql
@@ -35,14 +35,14 @@ select
     where
       (
         "assets"."type" = $1
-        and "assets"."isVisible" = $2
+        and "assets"."visibility" != $2
       )
   ) as "photos",
   count(*) filter (
     where
       (
         "assets"."type" = $3
-        and "assets"."isVisible" = $4
+        and "assets"."visibility" != $4
       )
   ) as "videos",
   coalesce(sum("exif"."fileSizeInByte"), $5) as "usage"
diff --git a/server/src/queries/map.repository.sql b/server/src/queries/map.repository.sql
index b3bb207946..edfdec13d2 100644
--- a/server/src/queries/map.repository.sql
+++ b/server/src/queries/map.repository.sql
@@ -14,7 +14,7 @@ from
   and "exif"."latitude" is not null
   and "exif"."longitude" is not null
 where
-  "isVisible" = $1
+  "assets"."visibility" = $1
   and "deletedAt" is null
   and (
     "ownerId" in ($2)
diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql
index f9ba32262d..09517a1e14 100644
--- a/server/src/queries/person.repository.sql
+++ b/server/src/queries/person.repository.sql
@@ -107,7 +107,7 @@ select
       (
         select
           "assets"."ownerId",
-          "assets"."isArchived",
+          "assets"."visibility",
           "assets"."fileCreatedAt"
         from
           "assets"
@@ -203,7 +203,7 @@ from
   "asset_faces"
   left join "assets" on "assets"."id" = "asset_faces"."assetId"
   and "asset_faces"."personId" = $1
-  and "assets"."isArchived" = $2
+  and "assets"."visibility" != $2
   and "assets"."deletedAt" is null
 where
   "asset_faces"."deletedAt" is null
@@ -220,7 +220,7 @@ from
   inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
   inner join "assets" on "assets"."id" = "asset_faces"."assetId"
   and "assets"."deletedAt" is null
-  and "assets"."isArchived" = $2
+  and "assets"."visibility" != $2
 where
   "person"."ownerId" = $3
   and "asset_faces"."deletedAt" is null
diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql
index 4fce272365..c18fe02418 100644
--- a/server/src/queries/search.repository.sql
+++ b/server/src/queries/search.repository.sql
@@ -7,11 +7,11 @@ from
   "assets"
   inner join "exif" on "assets"."id" = "exif"."assetId"
 where
-  "assets"."fileCreatedAt" >= $1
-  and "exif"."lensModel" = $2
-  and "assets"."ownerId" = any ($3::uuid[])
-  and "assets"."isFavorite" = $4
-  and "assets"."isArchived" = $5
+  "assets"."visibility" = $1
+  and "assets"."fileCreatedAt" >= $2
+  and "exif"."lensModel" = $3
+  and "assets"."ownerId" = any ($4::uuid[])
+  and "assets"."isFavorite" = $5
   and "assets"."deletedAt" is null
 order by
   "assets"."fileCreatedAt" desc
@@ -28,11 +28,11 @@ offset
     "assets"
     inner join "exif" on "assets"."id" = "exif"."assetId"
   where
-    "assets"."fileCreatedAt" >= $1
-    and "exif"."lensModel" = $2
-    and "assets"."ownerId" = any ($3::uuid[])
-    and "assets"."isFavorite" = $4
-    and "assets"."isArchived" = $5
+    "assets"."visibility" = $1
+    and "assets"."fileCreatedAt" >= $2
+    and "exif"."lensModel" = $3
+    and "assets"."ownerId" = any ($4::uuid[])
+    and "assets"."isFavorite" = $5
     and "assets"."deletedAt" is null
     and "assets"."id" < $6
   order by
@@ -48,11 +48,11 @@ union all
     "assets"
     inner join "exif" on "assets"."id" = "exif"."assetId"
   where
-    "assets"."fileCreatedAt" >= $8
-    and "exif"."lensModel" = $9
-    and "assets"."ownerId" = any ($10::uuid[])
-    and "assets"."isFavorite" = $11
-    and "assets"."isArchived" = $12
+    "assets"."visibility" = $8
+    and "assets"."fileCreatedAt" >= $9
+    and "exif"."lensModel" = $10
+    and "assets"."ownerId" = any ($11::uuid[])
+    and "assets"."isFavorite" = $12
     and "assets"."deletedAt" is null
     and "assets"."id" > $13
   order by
@@ -71,11 +71,11 @@ from
   inner join "exif" on "assets"."id" = "exif"."assetId"
   inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
 where
-  "assets"."fileCreatedAt" >= $1
-  and "exif"."lensModel" = $2
-  and "assets"."ownerId" = any ($3::uuid[])
-  and "assets"."isFavorite" = $4
-  and "assets"."isArchived" = $5
+  "assets"."visibility" = $1
+  and "assets"."fileCreatedAt" >= $2
+  and "exif"."lensModel" = $3
+  and "assets"."ownerId" = any ($4::uuid[])
+  and "assets"."isFavorite" = $5
   and "assets"."deletedAt" is null
 order by
   smart_search.embedding <=> $6
@@ -97,7 +97,7 @@ with
     where
       "assets"."ownerId" = any ($2::uuid[])
       and "assets"."deletedAt" is null
-      and "assets"."isVisible" = $3
+      and "assets"."visibility" != $3
       and "assets"."type" = $4
       and "assets"."id" != $5::uuid
       and "assets"."stackId" is null
@@ -176,14 +176,13 @@ with recursive
         inner join "assets" on "assets"."id" = "exif"."assetId"
       where
         "assets"."ownerId" = any ($1::uuid[])
-        and "assets"."isVisible" = $2
-        and "assets"."isArchived" = $3
-        and "assets"."type" = $4
+        and "assets"."visibility" = $2
+        and "assets"."type" = $3
         and "assets"."deletedAt" is null
       order by
         "city"
       limit
-        $5
+        $4
     )
     union all
     (
@@ -200,16 +199,15 @@ with recursive
             "exif"
             inner join "assets" on "assets"."id" = "exif"."assetId"
           where
-            "assets"."ownerId" = any ($6::uuid[])
-            and "assets"."isVisible" = $7
-            and "assets"."isArchived" = $8
-            and "assets"."type" = $9
+            "assets"."ownerId" = any ($5::uuid[])
+            and "assets"."visibility" = $6
+            and "assets"."type" = $7
             and "assets"."deletedAt" is null
             and "exif"."city" > "cte"."city"
           order by
             "city"
           limit
-            $10
+            $8
         ) as "l" on true
     )
   )
@@ -231,7 +229,7 @@ from
   inner join "assets" on "assets"."id" = "exif"."assetId"
 where
   "ownerId" = any ($1::uuid[])
-  and "isVisible" = $2
+  and "visibility" != $2
   and "deletedAt" is null
   and "state" is not null
 
@@ -243,7 +241,7 @@ from
   inner join "assets" on "assets"."id" = "exif"."assetId"
 where
   "ownerId" = any ($1::uuid[])
-  and "isVisible" = $2
+  and "visibility" != $2
   and "deletedAt" is null
   and "city" is not null
 
@@ -255,7 +253,7 @@ from
   inner join "assets" on "assets"."id" = "exif"."assetId"
 where
   "ownerId" = any ($1::uuid[])
-  and "isVisible" = $2
+  and "visibility" != $2
   and "deletedAt" is null
   and "make" is not null
 
@@ -267,6 +265,6 @@ from
   inner join "assets" on "assets"."id" = "exif"."assetId"
 where
   "ownerId" = any ($1::uuid[])
-  and "isVisible" = $2
+  and "visibility" != $2
   and "deletedAt" is null
   and "model" is not null
diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql
index e08335d9f1..54c1292d80 100644
--- a/server/src/queries/sync.repository.sql
+++ b/server/src/queries/sync.repository.sql
@@ -84,7 +84,7 @@ select
   "type",
   "deletedAt",
   "isFavorite",
-  "isVisible",
+  "visibility",
   "updateId"
 from
   "assets"
@@ -106,7 +106,7 @@ select
   "type",
   "deletedAt",
   "isFavorite",
-  "isVisible",
+  "visibility",
   "updateId"
 from
   "assets"
diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql
index e8ab5018fc..72881feea7 100644
--- a/server/src/queries/user.repository.sql
+++ b/server/src/queries/user.repository.sql
@@ -285,14 +285,14 @@ select
     where
       (
         "assets"."type" = 'IMAGE'
-        and "assets"."isVisible" = true
+        and "assets"."visibility" != 'hidden'
       )
   ) as "photos",
   count(*) filter (
     where
       (
         "assets"."type" = 'VIDEO'
-        and "assets"."isVisible" = true
+        and "assets"."visibility" != 'hidden'
       )
   ) as "videos",
   coalesce(
diff --git a/server/src/queries/view.repository.sql b/server/src/queries/view.repository.sql
index 1510521526..a2260ce5f6 100644
--- a/server/src/queries/view.repository.sql
+++ b/server/src/queries/view.repository.sql
@@ -7,8 +7,7 @@ from
   "assets"
 where
   "ownerId" = $2::uuid
-  and "isVisible" = $3
-  and "isArchived" = $4
+  and "visibility" = $3
   and "deletedAt" is null
   and "fileCreatedAt" is not null
   and "fileModifiedAt" is not null
@@ -23,13 +22,12 @@ from
   left join "exif" on "assets"."id" = "exif"."assetId"
 where
   "ownerId" = $1::uuid
-  and "isVisible" = $2
-  and "isArchived" = $3
+  and "visibility" = $2
   and "deletedAt" is null
   and "fileCreatedAt" is not null
   and "fileModifiedAt" is not null
   and "localDateTime" is not null
-  and "originalPath" like $4
-  and "originalPath" not like $5
+  and "originalPath" like $3
+  and "originalPath" not like $4
 order by
-  regexp_replace("assets"."originalPath", $6, $7) asc
+  regexp_replace("assets"."originalPath", $5, $6) asc
diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts
index c24209e482..5680ce2c64 100644
--- a/server/src/repositories/access.repository.ts
+++ b/server/src/repositories/access.repository.ts
@@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely';
 import { InjectKysely } from 'nestjs-kysely';
 import { DB } from 'src/db';
 import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
-import { AlbumUserRole } from 'src/enum';
+import { AlbumUserRole, AssetVisibility } from 'src/enum';
 import { asUuid } from 'src/utils/database';
 
 class ActivityAccess {
@@ -199,7 +199,13 @@ class AssetAccess {
       )
       .select('assets.id')
       .where('partner.sharedWithId', '=', userId)
-      .where('assets.isArchived', '=', false)
+      .where((eb) =>
+        eb.or([
+          eb('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)),
+          eb('assets.visibility', '=', sql.lit(AssetVisibility.HIDDEN)),
+        ]),
+      )
+
       .where('assets.id', 'in', [...assetIds])
       .execute()
       .then((assets) => new Set(assets.map((asset) => asset.id)));
diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts
index 1506f2997f..132bef6988 100644
--- a/server/src/repositories/asset-job.repository.ts
+++ b/server/src/repositories/asset-job.repository.ts
@@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
 import { Asset, columns } from 'src/database';
 import { DB } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
-import { AssetFileType, AssetType } from 'src/enum';
+import { AssetFileType, AssetType, AssetVisibility } from 'src/enum';
 import { StorageAsset } from 'src/types';
 import {
   anyUuid,
@@ -34,7 +34,7 @@ export class AssetJobRepository {
         'ownerId',
         'duplicateId',
         'stackId',
-        'isVisible',
+        'visibility',
         'smart_search.embedding',
         withFiles(eb, AssetFileType.PREVIEW),
       ])
@@ -70,7 +70,7 @@ export class AssetJobRepository {
       .select(['assets.id', 'assets.thumbhash'])
       .select(withFiles)
       .where('assets.deletedAt', 'is', null)
-      .where('assets.isVisible', '=', true)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
       .$if(!force, (qb) =>
         qb
           // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails
@@ -102,7 +102,7 @@ export class AssetJobRepository {
       .selectFrom('assets')
       .select([
         'assets.id',
-        'assets.isVisible',
+        'assets.visibility',
         'assets.originalFileName',
         'assets.originalPath',
         'assets.ownerId',
@@ -138,7 +138,7 @@ export class AssetJobRepository {
   private assetsWithPreviews() {
     return this.db
       .selectFrom('assets')
-      .where('assets.isVisible', '=', true)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
       .where('assets.deletedAt', 'is', null)
       .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
       .where('job_status.previewAt', 'is not', null);
@@ -169,7 +169,7 @@ export class AssetJobRepository {
   getForClipEncoding(id: string) {
     return this.db
       .selectFrom('assets')
-      .select(['assets.id', 'assets.isVisible'])
+      .select(['assets.id', 'assets.visibility'])
       .select((eb) => withFiles(eb, AssetFileType.PREVIEW))
       .where('assets.id', '=', id)
       .executeTakeFirst();
@@ -179,7 +179,7 @@ export class AssetJobRepository {
   getForDetectFacesJob(id: string) {
     return this.db
       .selectFrom('assets')
-      .select(['assets.id', 'assets.isVisible'])
+      .select(['assets.id', 'assets.visibility'])
       .$call(withExifInner)
       .select((eb) => withFaces(eb, true))
       .select((eb) => withFiles(eb, AssetFileType.PREVIEW))
@@ -209,7 +209,7 @@ export class AssetJobRepository {
       .selectFrom('assets')
       .select([
         'assets.id',
-        'assets.isVisible',
+        'assets.visibility',
         'assets.libraryId',
         'assets.ownerId',
         'assets.livePhotoVideoId',
@@ -228,7 +228,7 @@ export class AssetJobRepository {
             .select(['asset_stack.id', 'asset_stack.primaryAssetId'])
             .select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
             .where('stacked.deletedAt', 'is not', null)
-            .where('stacked.isArchived', '=', false)
+            .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
             .whereRef('stacked.stackId', '=', 'asset_stack.id')
             .groupBy('asset_stack.id')
             .as('stacked_assets'),
@@ -248,7 +248,7 @@ export class AssetJobRepository {
       .$if(!force, (qb) =>
         qb
           .where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')]))
-          .where('assets.isVisible', '=', true),
+          .where('assets.visibility', '!=', AssetVisibility.HIDDEN),
       )
       .where('assets.deletedAt', 'is', null)
       .stream();
@@ -275,7 +275,7 @@ export class AssetJobRepository {
           .where((eb) =>
             eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
           )
-          .where('assets.isVisible', '=', true),
+          .where('assets.visibility', '!=', AssetVisibility.HIDDEN),
       )
       .where('assets.deletedAt', 'is', null)
       .stream();
@@ -331,7 +331,7 @@ export class AssetJobRepository {
       .$if(!force, (qb) =>
         qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])),
       )
-      .where('assets.isVisible', '=', true)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
       .stream();
   }
 
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index 89062c210a..9bd115089f 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -6,7 +6,7 @@ import { Stack } from 'src/database';
 import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
 import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
 import { MapAsset } from 'src/dtos/asset-response.dto';
-import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
 import {
   anyUuid,
   asUuid,
@@ -14,6 +14,7 @@ import {
   removeUndefinedKeys,
   truncatedDate,
   unnest,
+  withDefaultVisibility,
   withExif,
   withFaces,
   withFacesAndPeople,
@@ -30,8 +31,8 @@ export type AssetStats = Record<AssetType, number>;
 
 export interface AssetStatsOptions {
   isFavorite?: boolean;
-  isArchived?: boolean;
   isTrashed?: boolean;
+  visibility?: AssetVisibility;
 }
 
 export interface LivePhotoSearchOptions {
@@ -52,7 +53,6 @@ export enum TimeBucketSize {
 }
 
 export interface AssetBuilderOptions {
-  isArchived?: boolean;
   isFavorite?: boolean;
   isTrashed?: boolean;
   isDuplicate?: boolean;
@@ -64,6 +64,7 @@ export interface AssetBuilderOptions {
   exifInfo?: boolean;
   status?: AssetStatus;
   assetType?: AssetType;
+  visibility?: AssetVisibility;
 }
 
 export interface TimeBucketOptions extends AssetBuilderOptions {
@@ -258,8 +259,7 @@ export class AssetRepository {
                 .where('asset_job_status.previewAt', 'is not', null)
                 .where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
                 .where('assets.ownerId', '=', anyUuid(ownerIds))
-                .where('assets.isVisible', '=', true)
-                .where('assets.isArchived', '=', false)
+                .where('assets.visibility', '=', AssetVisibility.TIMELINE)
                 .where((eb) =>
                   eb.exists((qb) =>
                     qb
@@ -348,7 +348,7 @@ export class AssetRepository {
       .select(['deviceAssetId'])
       .where('ownerId', '=', asUuid(ownerId))
       .where('deviceId', '=', deviceId)
-      .where('isVisible', '=', true)
+      .where('visibility', '!=', AssetVisibility.HIDDEN)
       .where('deletedAt', 'is', null)
       .execute();
 
@@ -393,7 +393,7 @@ export class AssetRepository {
                     .whereRef('stacked.stackId', '=', 'asset_stack.id')
                     .whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
                     .where('stacked.deletedAt', 'is', null)
-                    .where('stacked.isArchived', '=', false)
+                    .where('stacked.visibility', '=', AssetVisibility.TIMELINE)
                     .groupBy('asset_stack.id')
                     .as('stacked_assets'),
                 (join) => join.on('asset_stack.id', 'is not', null),
@@ -503,7 +503,7 @@ export class AssetRepository {
       .executeTakeFirst();
   }
 
-  getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
+  getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
     return this.db
       .selectFrom('assets')
       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO))
@@ -511,8 +511,8 @@ export class AssetRepository {
       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
       .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
       .where('ownerId', '=', asUuid(ownerId))
-      .where('isVisible', '=', true)
-      .$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!))
+      .$if(visibility === undefined, withDefaultVisibility)
+      .$if(!!visibility, (qb) => qb.where('assets.visibility', '=', visibility!))
       .$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
       .$if(!!isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
       .where('deletedAt', isTrashed ? 'is not' : 'is', null)
@@ -525,7 +525,7 @@ export class AssetRepository {
       .selectAll('assets')
       .$call(withExif)
       .where('ownerId', '=', anyUuid(userIds))
-      .where('isVisible', '=', true)
+      .where('visibility', '!=', AssetVisibility.HIDDEN)
       .where('deletedAt', 'is', null)
       .orderBy((eb) => eb.fn('random'))
       .limit(take)
@@ -542,7 +542,8 @@ export class AssetRepository {
             .select(truncatedDate<Date>(options.size).as('timeBucket'))
             .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
             .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
-            .where('assets.isVisible', '=', true)
+            .$if(options.visibility === undefined, withDefaultVisibility)
+            .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
             .$if(!!options.albumId, (qb) =>
               qb
                 .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
@@ -559,7 +560,6 @@ export class AssetRepository {
                 .where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])),
             )
             .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
-            .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
             .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
             .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
             .$if(options.isDuplicate !== undefined, (qb) =>
@@ -594,7 +594,6 @@ export class AssetRepository {
       )
       .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
       .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
-      .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
       .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
       .$if(!!options.withStacked, (qb) =>
         qb
@@ -610,7 +609,7 @@ export class AssetRepository {
                 .select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
                 .whereRef('stacked.stackId', '=', 'asset_stack.id')
                 .where('stacked.deletedAt', 'is', null)
-                .where('stacked.isArchived', '=', false)
+                .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
                 .groupBy('asset_stack.id')
                 .as('stacked_assets'),
             (join) => join.on('asset_stack.id', 'is not', null),
@@ -624,7 +623,8 @@ export class AssetRepository {
       .$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
       .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
       .where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
-      .where('assets.isVisible', '=', true)
+      .$if(options.visibility == undefined, withDefaultVisibility)
+      .$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
       .where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
       .orderBy('assets.localDateTime', options.order ?? 'desc')
       .execute();
@@ -658,7 +658,7 @@ export class AssetRepository {
             .where('assets.duplicateId', 'is not', null)
             .$narrowType<{ duplicateId: NotNull }>()
             .where('assets.deletedAt', 'is', null)
-            .where('assets.isVisible', '=', true)
+            .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
             .where('assets.stackId', 'is', null)
             .groupBy('assets.duplicateId'),
         )
@@ -703,8 +703,7 @@ export class AssetRepository {
       .select(['assetId as data', 'exif.city as value'])
       .$narrowType<{ value: NotNull }>()
       .where('ownerId', '=', asUuid(ownerId))
-      .where('isVisible', '=', true)
-      .where('isArchived', '=', false)
+      .where('visibility', '=', AssetVisibility.TIMELINE)
       .where('type', '=', AssetType.IMAGE)
       .where('deletedAt', 'is', null)
       .limit(maxFields)
@@ -743,7 +742,7 @@ export class AssetRepository {
       )
       .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack'))
       .where('assets.ownerId', '=', asUuid(ownerId))
-      .where('assets.isVisible', '=', true)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
       .where('assets.updatedAt', '<=', updatedUntil)
       .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
       .orderBy('assets.id')
@@ -771,7 +770,7 @@ export class AssetRepository {
       )
       .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack'))
       .where('assets.ownerId', '=', anyUuid(options.userIds))
-      .where('assets.isVisible', '=', true)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
       .where('assets.updatedAt', '>', options.updatedAfter)
       .limit(options.limit)
       .execute();
diff --git a/server/src/repositories/download.repository.ts b/server/src/repositories/download.repository.ts
index c9c62c90ce..4c4bed07ff 100644
--- a/server/src/repositories/download.repository.ts
+++ b/server/src/repositories/download.repository.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { Kysely } from 'kysely';
 import { InjectKysely } from 'nestjs-kysely';
 import { DB } from 'src/db';
+import { AssetVisibility } from 'src/enum';
 import { anyUuid } from 'src/utils/database';
 
 const builder = (db: Kysely<DB>) =>
@@ -31,6 +32,9 @@ export class DownloadRepository {
   }
 
   downloadUserId(userId: string) {
-    return builder(this.db).where('assets.ownerId', '=', userId).where('assets.isVisible', '=', true).stream();
+    return builder(this.db)
+      .where('assets.ownerId', '=', userId)
+      .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
+      .stream();
   }
 }
diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts
index fd9dd81b7b..b6c5ebbe08 100644
--- a/server/src/repositories/library.repository.ts
+++ b/server/src/repositories/library.repository.ts
@@ -4,7 +4,7 @@ import { InjectKysely } from 'nestjs-kysely';
 import { DB, Libraries } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
-import { AssetType } from 'src/enum';
+import { AssetType, AssetVisibility } from 'src/enum';
 
 export enum AssetSyncResult {
   DO_NOTHING,
@@ -77,13 +77,17 @@ export class LibraryRepository {
       .select((eb) =>
         eb.fn
           .countAll<number>()
-          .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
+          .filterWhere((eb) =>
+            eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]),
+          )
           .as('photos'),
       )
       .select((eb) =>
         eb.fn
           .countAll<number>()
-          .filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)]))
+          .filterWhere((eb) =>
+            eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]),
+          )
           .as('videos'),
       )
       .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage'))
diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts
index f9998ad179..3f559442aa 100644
--- a/server/src/repositories/map.repository.ts
+++ b/server/src/repositories/map.repository.ts
@@ -8,7 +8,7 @@ import readLine from 'node:readline';
 import { citiesFile } from 'src/constants';
 import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
-import { SystemMetadataKey } from 'src/enum';
+import { AssetVisibility, SystemMetadataKey } from 'src/enum';
 import { ConfigRepository } from 'src/repositories/config.repository';
 import { LoggingRepository } from 'src/repositories/logging.repository';
 import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
@@ -75,9 +75,11 @@ export class MapRepository {
   }
 
   @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] })
-  getMapMarkers(ownerIds: string[], albumIds: string[], options: MapMarkerSearchOptions = {}) {
-    const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
-
+  getMapMarkers(
+    ownerIds: string[],
+    albumIds: string[],
+    { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {},
+  ) {
     return this.db
       .selectFrom('assets')
       .innerJoin('exif', (builder) =>
@@ -88,8 +90,17 @@ export class MapRepository {
       )
       .select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country'])
       .$narrowType<{ lat: NotNull; lon: NotNull }>()
-      .where('isVisible', '=', true)
-      .$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!))
+      .$if(isArchived === true, (qb) =>
+        qb.where((eb) =>
+          eb.or([
+            eb('assets.visibility', '=', AssetVisibility.TIMELINE),
+            eb('assets.visibility', '=', AssetVisibility.ARCHIVE),
+          ]),
+        ),
+      )
+      .$if(isArchived === false || isArchived === undefined, (qb) =>
+        qb.where('assets.visibility', '=', AssetVisibility.TIMELINE),
+      )
       .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!))
       .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!))
       .$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!))
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 0383a54a27..b55537bdba 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -4,7 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
 import { InjectKysely } from 'nestjs-kysely';
 import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
 import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
-import { AssetFileType, SourceType } from 'src/enum';
+import { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
 import { removeUndefinedKeys } from 'src/utils/database';
 import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
 
@@ -157,7 +157,7 @@ export class PersonRepository {
       .innerJoin('assets', (join) =>
         join
           .onRef('asset_faces.assetId', '=', 'assets.id')
-          .on('assets.isArchived', '=', false)
+          .on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
           .on('assets.deletedAt', 'is', null),
       )
       .where('person.ownerId', '=', userId)
@@ -248,7 +248,7 @@ export class PersonRepository {
         jsonObjectFrom(
           eb
             .selectFrom('assets')
-            .select(['assets.ownerId', 'assets.isArchived', 'assets.fileCreatedAt'])
+            .select(['assets.ownerId', 'assets.visibility', 'assets.fileCreatedAt'])
             .whereRef('assets.id', '=', 'asset_faces.assetId'),
         ).as('asset'),
       )
@@ -346,7 +346,7 @@ export class PersonRepository {
         join
           .onRef('assets.id', '=', 'asset_faces.assetId')
           .on('asset_faces.personId', '=', personId)
-          .on('assets.isArchived', '=', false)
+          .on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
           .on('assets.deletedAt', 'is', null),
       )
       .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
@@ -369,7 +369,7 @@ export class PersonRepository {
         join
           .onRef('assets.id', '=', 'asset_faces.assetId')
           .on('assets.deletedAt', 'is', null)
-          .on('assets.isArchived', '=', false),
+          .on('assets.visibility', '!=', AssetVisibility.ARCHIVE),
       )
       .select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
       .select((eb) =>
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index b991ecc78b..4e6b6e0fcf 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
 import { DB, Exif } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
 import { MapAsset } from 'src/dtos/asset-response.dto';
-import { AssetStatus, AssetType } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
 import { ConfigRepository } from 'src/repositories/config.repository';
 import { anyUuid, asUuid, searchAssetBuilder, vectorIndexQuery } from 'src/utils/database';
 import { paginationHelper } from 'src/utils/pagination';
@@ -26,17 +26,16 @@ export interface SearchUserIdOptions {
 export type SearchIdOptions = SearchAssetIdOptions & SearchUserIdOptions;
 
 export interface SearchStatusOptions {
-  isArchived?: boolean;
   isEncoded?: boolean;
   isFavorite?: boolean;
   isMotion?: boolean;
   isOffline?: boolean;
-  isVisible?: boolean;
   isNotInAlbum?: boolean;
   type?: AssetType;
   status?: AssetStatus;
   withArchived?: boolean;
   withDeleted?: boolean;
+  visibility?: AssetVisibility;
 }
 
 export interface SearchOneToOneRelationOptions {
@@ -276,7 +275,7 @@ export class SearchRepository {
           .innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
           .where('assets.ownerId', '=', anyUuid(userIds))
           .where('assets.deletedAt', 'is', null)
-          .where('assets.isVisible', '=', true)
+          .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
           .where('assets.type', '=', type)
           .where('assets.id', '!=', asUuid(assetId))
           .where('assets.stackId', 'is', null)
@@ -367,8 +366,7 @@ export class SearchRepository {
           .select(['city', 'assetId'])
           .innerJoin('assets', 'assets.id', 'exif.assetId')
           .where('assets.ownerId', '=', anyUuid(userIds))
-          .where('assets.isVisible', '=', true)
-          .where('assets.isArchived', '=', false)
+          .where('assets.visibility', '=', AssetVisibility.TIMELINE)
           .where('assets.type', '=', AssetType.IMAGE)
           .where('assets.deletedAt', 'is', null)
           .orderBy('city')
@@ -384,8 +382,7 @@ export class SearchRepository {
                 .select(['city', 'assetId'])
                 .innerJoin('assets', 'assets.id', 'exif.assetId')
                 .where('assets.ownerId', '=', anyUuid(userIds))
-                .where('assets.isVisible', '=', true)
-                .where('assets.isArchived', '=', false)
+                .where('assets.visibility', '=', AssetVisibility.TIMELINE)
                 .where('assets.type', '=', AssetType.IMAGE)
                 .where('assets.deletedAt', 'is', null)
                 .whereRef('exif.city', '>', 'cte.city')
@@ -518,7 +515,7 @@ export class SearchRepository {
       .distinctOn(field)
       .innerJoin('assets', 'assets.id', 'exif.assetId')
       .where('ownerId', '=', anyUuid(userIds))
-      .where('isVisible', '=', true)
+      .where('visibility', '!=', AssetVisibility.HIDDEN)
       .where('deletedAt', 'is', null)
       .where(field, 'is not', null);
   }
diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts
index e2e396f7b2..4d7671ca92 100644
--- a/server/src/repositories/user.repository.ts
+++ b/server/src/repositories/user.repository.ts
@@ -6,7 +6,7 @@ import { InjectKysely } from 'nestjs-kysely';
 import { columns } from 'src/database';
 import { DB, UserMetadata as DbUserMetadata } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
-import { AssetType, UserStatus } from 'src/enum';
+import { AssetType, AssetVisibility, UserStatus } from 'src/enum';
 import { UserTable } from 'src/schema/tables/user.table';
 import { UserMetadata, UserMetadataItem } from 'src/types';
 import { asUuid } from 'src/utils/database';
@@ -205,13 +205,19 @@ export class UserRepository {
         eb.fn
           .countAll<number>()
           .filterWhere((eb) =>
-            eb.and([eb('assets.type', '=', sql.lit(AssetType.IMAGE)), eb('assets.isVisible', '=', sql.lit(true))]),
+            eb.and([
+              eb('assets.type', '=', sql.lit(AssetType.IMAGE)),
+              eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)),
+            ]),
           )
           .as('photos'),
         eb.fn
           .countAll<number>()
           .filterWhere((eb) =>
-            eb.and([eb('assets.type', '=', sql.lit(AssetType.VIDEO)), eb('assets.isVisible', '=', sql.lit(true))]),
+            eb.and([
+              eb('assets.type', '=', sql.lit(AssetType.VIDEO)),
+              eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)),
+            ]),
           )
           .as('videos'),
         eb.fn
diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts
index e32933065c..03e8b3763f 100644
--- a/server/src/repositories/view-repository.ts
+++ b/server/src/repositories/view-repository.ts
@@ -2,6 +2,7 @@ import { Kysely } from 'kysely';
 import { InjectKysely } from 'nestjs-kysely';
 import { DB } from 'src/db';
 import { DummyValue, GenerateSql } from 'src/decorators';
+import { AssetVisibility } from 'src/enum';
 import { asUuid, withExif } from 'src/utils/database';
 
 export class ViewRepository {
@@ -14,8 +15,7 @@ export class ViewRepository {
       .select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
       .distinct()
       .where('ownerId', '=', asUuid(userId))
-      .where('isVisible', '=', true)
-      .where('isArchived', '=', false)
+      .where('visibility', '=', AssetVisibility.TIMELINE)
       .where('deletedAt', 'is', null)
       .where('fileCreatedAt', 'is not', null)
       .where('fileModifiedAt', 'is not', null)
@@ -34,8 +34,7 @@ export class ViewRepository {
       .selectAll('assets')
       .$call(withExif)
       .where('ownerId', '=', asUuid(userId))
-      .where('isVisible', '=', true)
-      .where('isArchived', '=', false)
+      .where('visibility', '=', AssetVisibility.TIMELINE)
       .where('deletedAt', 'is', null)
       .where('fileCreatedAt', 'is not', null)
       .where('fileModifiedAt', 'is not', null)
diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts
index c62681d049..1800f08c13 100644
--- a/server/src/schema/index.ts
+++ b/server/src/schema/index.ts
@@ -1,3 +1,4 @@
+import { AssetVisibility } from 'src/enum';
 import { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
 import {
   assets_delete_audit,
@@ -45,7 +46,12 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table';
 import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
 import { UserTable } from 'src/schema/tables/user.table';
 import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
-import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
+import { ConfigurationParameter, Database, Extensions, registerEnum } from 'src/sql-tools';
+
+export const asset_visibility_enum = registerEnum({
+  name: 'asset_visibility_enum',
+  values: Object.values(AssetVisibility),
+});
 
 @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
 @ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
diff --git a/server/src/schema/migrations/1745902563899-AddAssetVisibilityColumn.ts b/server/src/schema/migrations/1745902563899-AddAssetVisibilityColumn.ts
new file mode 100644
index 0000000000..6fe9dab1a0
--- /dev/null
+++ b/server/src/schema/migrations/1745902563899-AddAssetVisibilityColumn.ts
@@ -0,0 +1,37 @@
+import { Kysely, sql } from 'kysely';
+
+export async function up(db: Kysely<any>): Promise<void> {
+  await sql`CREATE TYPE "asset_visibility_enum" AS ENUM ('archive','timeline','hidden');`.execute(db);
+  await sql`ALTER TABLE "assets"
+    ADD "visibility" asset_visibility_enum NOT NULL DEFAULT 'timeline';`.execute(db);
+
+  await sql`
+    UPDATE "assets"
+    SET "visibility" = CASE
+                        WHEN "isArchived" THEN 'archive'::asset_visibility_enum
+                        WHEN "isVisible" THEN 'timeline'::asset_visibility_enum
+                        ELSE 'hidden'::asset_visibility_enum
+                      END;
+  `.execute(db);
+
+  await sql`ALTER TABLE "assets" DROP COLUMN "isVisible";`.execute(db);
+  await sql`ALTER TABLE "assets" DROP COLUMN "isArchived";`.execute(db);
+}
+
+export async function down(db: Kysely<any>): Promise<void> {
+  await sql`ALTER TABLE "assets" ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT FALSE;`.execute(db);
+  await sql`ALTER TABLE "assets" ADD COLUMN "isVisible" BOOLEAN NOT NULL DEFAULT TRUE;`.execute(db);
+
+  await sql`
+      UPDATE "assets"
+      SET
+        "isArchived" = ("visibility" = 'archive'::asset_visibility_enum),
+        "isVisible" = CASE
+                        WHEN "visibility" = 'timeline'::asset_visibility_enum THEN TRUE
+                        WHEN "visibility" = 'archive'::asset_visibility_enum THEN TRUE
+                        ELSE FALSE
+                      END;
+    `.execute(db);
+  await sql`ALTER TABLE "assets" DROP COLUMN "visibility";`.execute(db);
+  await sql`DROP TYPE "asset_visibility_enum";`.execute(db);
+}
diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts
index 19ec8d2ef4..4552ac158d 100644
--- a/server/src/schema/tables/asset.table.ts
+++ b/server/src/schema/tables/asset.table.ts
@@ -1,5 +1,6 @@
 import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
-import { AssetStatus, AssetType } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
+import { asset_visibility_enum } from 'src/schema';
 import { assets_status_enum } from 'src/schema/enums';
 import { assets_delete_audit } from 'src/schema/functions';
 import { LibraryTable } from 'src/schema/tables/library.table';
@@ -95,9 +96,6 @@ export class AssetTable {
   @Column({ type: 'bytea', index: true })
   checksum!: Buffer; // sha1 checksum
 
-  @Column({ type: 'boolean', default: true })
-  isVisible!: boolean;
-
   @ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
   livePhotoVideoId!: string | null;
 
@@ -107,9 +105,6 @@ export class AssetTable {
   @CreateDateColumn()
   createdAt!: Date;
 
-  @Column({ type: 'boolean', default: false })
-  isArchived!: boolean;
-
   @Column({ index: true })
   originalFileName!: string;
 
@@ -145,4 +140,7 @@ export class AssetTable {
 
   @UpdateIdColumn({ indexName: 'IDX_assets_update_id' })
   updateId?: string;
+
+  @Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE })
+  visibility!: AssetVisibility;
 }
diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts
index d25067f1c9..8490e8aaea 100644
--- a/server/src/services/asset-media.service.spec.ts
+++ b/server/src/services/asset-media.service.spec.ts
@@ -9,7 +9,7 @@ import { AssetFile } from 'src/database';
 import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
 import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
 import { MapAsset } from 'src/dtos/asset-response.dto';
-import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
+import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
 import { AuthRequest } from 'src/middleware/auth.guard';
 import { AssetMediaService } from 'src/services/asset-media.service';
 import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
@@ -142,7 +142,6 @@ const createDto = Object.freeze({
   fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
   fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
   isFavorite: false,
-  isArchived: false,
   duration: '0:00:00.000000',
 }) as AssetMediaCreateDto;
 
@@ -164,7 +163,6 @@ const assetEntity = Object.freeze({
   fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
   updatedAt: new Date('2022-06-19T23:41:36.910Z'),
   isFavorite: false,
-  isArchived: false,
   encodedVideoPath: '',
   duration: '0:00:00.000000',
   files: [] as AssetFile[],
@@ -437,7 +435,10 @@ describe(AssetMediaService.name, () => {
     });
 
     it('should hide the linked motion asset', async () => {
-      mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true });
+      mocks.asset.getById.mockResolvedValueOnce({
+        ...assetStub.livePhotoMotionAsset,
+        visibility: AssetVisibility.TIMELINE,
+      });
       mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
 
       await expect(
@@ -452,7 +453,10 @@ describe(AssetMediaService.name, () => {
       });
 
       expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
-      expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
+      expect(mocks.asset.update).toHaveBeenCalledWith({
+        id: 'live-photo-motion-asset',
+        visibility: AssetVisibility.HIDDEN,
+      });
     });
 
     it('should handle a sidecar file', async () => {
diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts
index 78e23fa802..87d617ede6 100644
--- a/server/src/services/asset-media.service.ts
+++ b/server/src/services/asset-media.service.ts
@@ -21,7 +21,7 @@ import {
   UploadFieldName,
 } from 'src/dtos/asset-media.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
 import { AuthRequest } from 'src/middleware/auth.guard';
 import { BaseService } from 'src/services/base.service';
 import { UploadFile } from 'src/types';
@@ -146,7 +146,6 @@ export class AssetMediaService extends BaseService {
           { userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
         );
       }
-
       const asset = await this.create(auth.user.id, dto, file, sidecarFile);
 
       await this.userRepository.updateUsage(auth.user.id, file.size);
@@ -416,9 +415,8 @@ export class AssetMediaService extends BaseService {
 
       type: mimeTypes.assetType(file.originalPath),
       isFavorite: dto.isFavorite,
-      isArchived: dto.isArchived ?? false,
       duration: dto.duration || null,
-      isVisible: dto.isVisible ?? true,
+      visibility: dto.visibility ?? AssetVisibility.TIMELINE,
       livePhotoVideoId: dto.livePhotoVideoId,
       originalFileName: file.originalName,
       sidecarPath: sidecarFile?.originalPath,
diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts
index b677a6588d..1e4cfddcf5 100755
--- a/server/src/services/asset.service.spec.ts
+++ b/server/src/services/asset.service.spec.ts
@@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
 import { DateTime } from 'luxon';
 import { MapAsset } from 'src/dtos/asset-response.dto';
 import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
-import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
 import { AssetStats } from 'src/repositories/asset.repository';
 import { AssetService } from 'src/services/asset.service';
 import { assetStub } from 'test/fixtures/asset.stub';
@@ -46,14 +46,22 @@ describe(AssetService.name, () => {
   describe('getStatistics', () => {
     it('should get the statistics for a user, excluding archived assets', async () => {
       mocks.asset.getStatistics.mockResolvedValue(stats);
-      await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
-      expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
+      await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual(
+        statResponse,
+      );
+      expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
+        visibility: AssetVisibility.TIMELINE,
+      });
     });
 
     it('should get the statistics for a user for archived assets', async () => {
       mocks.asset.getStatistics.mockResolvedValue(stats);
-      await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
-      expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
+      await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual(
+        statResponse,
+      );
+      expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
+        visibility: AssetVisibility.ARCHIVE,
+      });
     });
 
     it('should get the statistics for a user for favorite assets', async () => {
@@ -192,9 +200,9 @@ describe(AssetService.name, () => {
 
   describe('update', () => {
     it('should require asset write access for the id', async () => {
-      await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
-        BadRequestException,
-      );
+      await expect(
+        sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }),
+      ).rejects.toBeInstanceOf(BadRequestException);
 
       expect(mocks.asset.update).not.toHaveBeenCalled();
     });
@@ -242,7 +250,10 @@ describe(AssetService.name, () => {
         id: assetStub.livePhotoStillAsset.id,
         livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
       });
-      expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(mocks.asset.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: AssetVisibility.TIMELINE,
+      });
       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
         assetId: assetStub.livePhotoMotionAsset.id,
         userId: userStub.admin.id,
@@ -263,7 +274,10 @@ describe(AssetService.name, () => {
         id: assetStub.livePhotoStillAsset.id,
         livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
       });
-      expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(mocks.asset.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: AssetVisibility.TIMELINE,
+      });
       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
         assetId: assetStub.livePhotoMotionAsset.id,
         userId: userStub.admin.id,
@@ -284,7 +298,10 @@ describe(AssetService.name, () => {
         id: assetStub.livePhotoStillAsset.id,
         livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
       });
-      expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(mocks.asset.update).not.toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: AssetVisibility.TIMELINE,
+      });
       expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
         assetId: assetStub.livePhotoMotionAsset.id,
         userId: userStub.admin.id,
@@ -296,7 +313,7 @@ describe(AssetService.name, () => {
       mocks.asset.getById.mockResolvedValueOnce({
         ...assetStub.livePhotoMotionAsset,
         ownerId: authStub.admin.user.id,
-        isVisible: true,
+        visibility: AssetVisibility.TIMELINE,
       });
       mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
       mocks.asset.update.mockResolvedValue(assetStub.image);
@@ -305,7 +322,10 @@ describe(AssetService.name, () => {
         livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
       });
 
-      expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
+      expect(mocks.asset.update).toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: AssetVisibility.HIDDEN,
+      });
       expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
         assetId: assetStub.livePhotoMotionAsset.id,
         userId: userStub.admin.id,
@@ -335,7 +355,10 @@ describe(AssetService.name, () => {
         id: assetStub.livePhotoStillAsset.id,
         livePhotoVideoId: null,
       });
-      expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
+      expect(mocks.asset.update).toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: assetStub.livePhotoStillAsset.visibility,
+      });
       expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
         assetId: assetStub.livePhotoMotionAsset.id,
         userId: userStub.admin.id,
@@ -361,7 +384,6 @@ describe(AssetService.name, () => {
       await expect(
         sut.updateAll(authStub.admin, {
           ids: ['asset-1'],
-          isArchived: false,
         }),
       ).rejects.toBeInstanceOf(BadRequestException);
     });
@@ -369,9 +391,11 @@ describe(AssetService.name, () => {
     it('should update all assets', async () => {
       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
 
-      await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
+      await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE });
 
-      expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
+      expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
+        visibility: AssetVisibility.ARCHIVE,
+      });
     });
 
     it('should not update Assets table if no relevant fields are provided', async () => {
@@ -381,7 +405,6 @@ describe(AssetService.name, () => {
         ids: ['asset-1'],
         latitude: 0,
         longitude: 0,
-        isArchived: undefined,
         isFavorite: undefined,
         duplicateId: undefined,
         rating: undefined,
@@ -389,14 +412,14 @@ describe(AssetService.name, () => {
       expect(mocks.asset.updateAll).not.toHaveBeenCalled();
     });
 
-    it('should update Assets table if isArchived field is provided', async () => {
+    it('should update Assets table if visibility field is provided', async () => {
       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
 
       await sut.updateAll(authStub.admin, {
         ids: ['asset-1'],
         latitude: 0,
         longitude: 0,
-        isArchived: undefined,
+        visibility: undefined,
         isFavorite: false,
         duplicateId: undefined,
         rating: undefined,
@@ -416,7 +439,6 @@ describe(AssetService.name, () => {
         latitude: 30,
         longitude: 50,
         dateTimeOriginal,
-        isArchived: undefined,
         isFavorite: false,
         duplicateId: undefined,
         rating: undefined,
@@ -439,7 +461,6 @@ describe(AssetService.name, () => {
         ids: ['asset-1'],
         latitude: 0,
         longitude: 0,
-        isArchived: undefined,
         isFavorite: undefined,
         duplicateId: null,
         rating: undefined,
diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts
index 6047130546..3ab6fcb8a7 100644
--- a/server/src/services/asset.service.ts
+++ b/server/src/services/asset.service.ts
@@ -92,8 +92,12 @@ export class AssetService extends BaseService {
 
     const asset = await this.assetRepository.update({ id, ...rest });
 
-    if (previousMotion) {
-      await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
+    if (previousMotion && asset) {
+      await onAfterUnlink(repos, {
+        userId: auth.user.id,
+        livePhotoVideoId: previousMotion.id,
+        visibility: asset.visibility,
+      });
     }
 
     if (!asset) {
@@ -115,7 +119,7 @@ export class AssetService extends BaseService {
     }
 
     if (
-      options.isArchived !== undefined ||
+      options.visibility !== undefined ||
       options.isFavorite !== undefined ||
       options.duplicateId !== undefined ||
       options.rating !== undefined
diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts
index ed8f2cf177..3f08e36a21 100644
--- a/server/src/services/duplicate.service.spec.ts
+++ b/server/src/services/duplicate.service.spec.ts
@@ -1,4 +1,4 @@
-import { AssetFileType, AssetType, JobName, JobStatus } from 'src/enum';
+import { AssetFileType, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
 import { DuplicateService } from 'src/services/duplicate.service';
 import { SearchService } from 'src/services/search.service';
 import { assetStub } from 'test/fixtures/asset.stub';
@@ -22,11 +22,11 @@ const hasEmbedding = {
       updateId: 'update-1',
     },
   ],
-  isVisible: true,
   stackId: null,
   type: AssetType.IMAGE,
   duplicateId: null,
   embedding: '[1, 2, 3, 4]',
+  visibility: AssetVisibility.TIMELINE,
 };
 
 const hasDupe = {
@@ -207,7 +207,10 @@ describe(SearchService.name, () => {
 
     it('should skip if asset is not visible', async () => {
       const id = assetStub.livePhotoMotionAsset.id;
-      mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, isVisible: false });
+      mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({
+        ...hasEmbedding,
+        visibility: AssetVisibility.HIDDEN,
+      });
 
       const result = await sut.handleSearchDuplicates({ id });
 
diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts
index 41e3f13c4d..b5e4f573f2 100644
--- a/server/src/services/duplicate.service.ts
+++ b/server/src/services/duplicate.service.ts
@@ -4,7 +4,7 @@ import { OnJob } from 'src/decorators';
 import { mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
-import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
+import { AssetFileType, AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum';
 import { AssetDuplicateResult } from 'src/repositories/search.repository';
 import { BaseService } from 'src/services/base.service';
 import { JobItem, JobOf } from 'src/types';
@@ -65,7 +65,7 @@ export class DuplicateService extends BaseService {
       return JobStatus.SKIPPED;
     }
 
-    if (!asset.isVisible) {
+    if (asset.visibility == AssetVisibility.HIDDEN) {
       this.logger.debug(`Asset ${id} is not visible, skipping`);
       return JobStatus.SKIPPED;
     }
diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts
index cf9b87f4e6..fd573d9b97 100644
--- a/server/src/services/job.service.ts
+++ b/server/src/services/job.service.ts
@@ -6,6 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
 import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
 import {
   AssetType,
+  AssetVisibility,
   BootstrapEventPriority,
   ImmichWorker,
   JobCommand,
@@ -301,7 +302,7 @@ export class JobService extends BaseService {
         }
 
         await this.jobRepository.queueAll(jobs);
-        if (asset.isVisible) {
+        if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) {
           this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
         }
 
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index 546dcc930b..adc8c4b904 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -8,6 +8,7 @@ import {
   AssetFileType,
   AssetPathType,
   AssetType,
+  AssetVisibility,
   AudioCodec,
   Colorspace,
   JobName,
@@ -152,7 +153,7 @@ export class MediaService extends BaseService {
       return JobStatus.FAILED;
     }
 
-    if (!asset.isVisible) {
+    if (asset.visibility === AssetVisibility.HIDDEN) {
       this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`);
       return JobStatus.SKIPPED;
     }
diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
index b048923b38..28cb42a16b 100644
--- a/server/src/services/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -4,7 +4,7 @@ import { Stats } from 'node:fs';
 import { constants } from 'node:fs/promises';
 import { defaults } from 'src/config';
 import { MapAsset } from 'src/dtos/asset-response.dto';
-import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
+import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
 import { ImmichTags } from 'src/repositories/metadata.repository';
 import { MetadataService } from 'src/services/metadata.service';
 import { assetStub } from 'test/fixtures/asset.stub';
@@ -504,7 +504,10 @@ describe(MetadataService.name, () => {
     });
 
     it('should not apply motion photos if asset is video', async () => {
-      mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
+      mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
+        ...assetStub.livePhotoMotionAsset,
+        visibility: AssetVisibility.TIMELINE,
+      });
       mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
 
       await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
@@ -513,7 +516,7 @@ describe(MetadataService.name, () => {
       expect(mocks.job.queue).not.toHaveBeenCalled();
       expect(mocks.job.queueAll).not.toHaveBeenCalled();
       expect(mocks.asset.update).not.toHaveBeenCalledWith(
-        expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
+        expect.objectContaining({ assetType: AssetType.VIDEO, visibility: AssetVisibility.HIDDEN }),
       );
     });
 
@@ -580,7 +583,7 @@ describe(MetadataService.name, () => {
         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
         id: fileStub.livePhotoMotion.uuid,
-        isVisible: false,
+        visibility: AssetVisibility.HIDDEN,
         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         originalFileName: 'asset_1.mp4',
@@ -638,7 +641,7 @@ describe(MetadataService.name, () => {
         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
         id: fileStub.livePhotoMotion.uuid,
-        isVisible: false,
+        visibility: AssetVisibility.HIDDEN,
         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         originalFileName: 'asset_1.mp4',
@@ -696,7 +699,7 @@ describe(MetadataService.name, () => {
         fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
         id: fileStub.livePhotoMotion.uuid,
-        isVisible: false,
+        visibility: AssetVisibility.HIDDEN,
         libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
         localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
         originalFileName: 'asset_1.mp4',
@@ -773,14 +776,17 @@ describe(MetadataService.name, () => {
         MicroVideoOffset: 1,
       });
       mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
-      mocks.asset.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
+      mocks.asset.getByChecksum.mockResolvedValue({
+        ...assetStub.livePhotoMotionAsset,
+        visibility: AssetVisibility.TIMELINE,
+      });
       const video = randomBytes(512);
       mocks.storage.readFile.mockResolvedValue(video);
 
       await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
       expect(mocks.asset.update).toHaveBeenCalledWith({
         id: assetStub.livePhotoMotionAsset.id,
-        isVisible: false,
+        visibility: AssetVisibility.HIDDEN,
       });
       expect(mocks.asset.update).toHaveBeenCalledWith({
         id: assetStub.livePhotoStillAsset.id,
@@ -1301,7 +1307,9 @@ describe(MetadataService.name, () => {
 
       expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
       expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
-      expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
+      expect(mocks.asset.update).not.toHaveBeenCalledWith(
+        expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
+      );
       expect(mocks.album.removeAsset).not.toHaveBeenCalled();
     });
 
@@ -1320,7 +1328,9 @@ describe(MetadataService.name, () => {
         libraryId: null,
         type: AssetType.IMAGE,
       });
-      expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
+      expect(mocks.asset.update).not.toHaveBeenCalledWith(
+        expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
+      );
       expect(mocks.album.removeAsset).not.toHaveBeenCalled();
     });
 
@@ -1342,7 +1352,10 @@ describe(MetadataService.name, () => {
         id: assetStub.livePhotoStillAsset.id,
         livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
       });
-      expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
+      expect(mocks.asset.update).toHaveBeenCalledWith({
+        id: assetStub.livePhotoMotionAsset.id,
+        visibility: AssetVisibility.HIDDEN,
+      });
       expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
     });
 
diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts
index 17f3325f99..3497b808da 100644
--- a/server/src/services/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -14,6 +14,7 @@ import { AssetFaces, Exif, Person } from 'src/db';
 import { OnEvent, OnJob } from 'src/decorators';
 import {
   AssetType,
+  AssetVisibility,
   DatabaseLock,
   ExifOrientation,
   ImmichWorker,
@@ -156,7 +157,7 @@ export class MetadataService extends BaseService {
     const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
     await Promise.all([
       this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
-      this.assetRepository.update({ id: motionAsset.id, isVisible: false }),
+      this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }),
       this.albumRepository.removeAsset(motionAsset.id),
     ]);
 
@@ -527,8 +528,11 @@ export class MetadataService extends BaseService {
         });
 
         // Hide the motion photo video asset if it's not already hidden to prepare for linking
-        if (motionAsset.isVisible) {
-          await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
+        if (motionAsset.visibility === AssetVisibility.TIMELINE) {
+          await this.assetRepository.update({
+            id: motionAsset.id,
+            visibility: AssetVisibility.HIDDEN,
+          });
           this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
         }
       } else {
@@ -544,7 +548,7 @@ export class MetadataService extends BaseService {
           ownerId: asset.ownerId,
           originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
           originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
-          isVisible: false,
+          visibility: AssetVisibility.HIDDEN,
           deviceAssetId: 'NONE',
           deviceId: 'NONE',
         });
@@ -863,7 +867,7 @@ export class MetadataService extends BaseService {
       return JobStatus.FAILED;
     }
 
-    if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
+    if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) {
       return JobStatus.FAILED;
     }
 
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index 227ea3c1c2..77a9c70300 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -26,6 +26,7 @@ import {
 } from 'src/dtos/person.dto';
 import {
   AssetType,
+  AssetVisibility,
   CacheControl,
   ImageFormat,
   JobName,
@@ -296,7 +297,7 @@ export class PersonService extends BaseService {
       return JobStatus.FAILED;
     }
 
-    if (!asset.isVisible) {
+    if (asset.visibility === AssetVisibility.HIDDEN) {
       return JobStatus.SKIPPED;
     }
 
@@ -484,7 +485,9 @@ export class PersonService extends BaseService {
 
     this.logger.debug(`Face ${id} has ${matches.length} matches`);
 
-    const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived;
+    const isCore =
+      matches.length >= machineLearning.facialRecognition.minFaces &&
+      face.asset.visibility === AssetVisibility.TIMELINE;
     if (!isCore && !deferred) {
       this.logger.debug(`Deferring non-core face ${id} for later processing`);
       await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts
index 5ee5dac57e..f3702c2010 100644
--- a/server/src/services/smart-info.service.ts
+++ b/server/src/services/smart-info.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { SystemConfig } from 'src/config';
 import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
 import { OnEvent, OnJob } from 'src/decorators';
-import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
+import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
 import { ArgOf } from 'src/repositories/event.repository';
 import { BaseService } from 'src/services/base.service';
 import { JobItem, JobOf } from 'src/types';
@@ -104,7 +104,7 @@ export class SmartInfoService extends BaseService {
       return JobStatus.FAILED;
     }
 
-    if (!asset.isVisible) {
+    if (asset.visibility === AssetVisibility.HIDDEN) {
       return JobStatus.SKIPPED;
     }
 
diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts
index c88348b39e..6ad488c48d 100644
--- a/server/src/services/sync.service.ts
+++ b/server/src/services/sync.service.ts
@@ -14,7 +14,7 @@ import {
   SyncAckSetDto,
   SyncStreamDto,
 } from 'src/dtos/sync.dto';
-import { DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
+import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
 import { BaseService } from 'src/services/base.service';
 import { SyncAck } from 'src/types';
 import { getMyPartnerIds } from 'src/utils/asset.util';
@@ -262,7 +262,10 @@ export class SyncService extends BaseService {
       needsFullSync: false,
       upserted: upserted
         // do not return archived assets for partner users
-        .filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived))
+        .filter(
+          (a) =>
+            a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.TIMELINE),
+        )
         .map((a) =>
           mapAsset(a, {
             auth,
diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts
index c6a09d2fdf..1447594d4e 100644
--- a/server/src/services/timeline.service.spec.ts
+++ b/server/src/services/timeline.service.spec.ts
@@ -1,4 +1,5 @@
 import { BadRequestException } from '@nestjs/common';
+import { AssetVisibility } from 'src/enum';
 import { TimeBucketSize } from 'src/repositories/asset.repository';
 import { TimelineService } from 'src/services/timeline.service';
 import { assetStub } from 'test/fixtures/asset.stub';
@@ -54,7 +55,7 @@ describe(TimelineService.name, () => {
         sut.getTimeBucket(authStub.admin, {
           size: TimeBucketSize.DAY,
           timeBucket: 'bucket',
-          isArchived: true,
+          visibility: AssetVisibility.ARCHIVE,
           userId: authStub.admin.user.id,
         }),
       ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
@@ -63,7 +64,7 @@ describe(TimelineService.name, () => {
         expect.objectContaining({
           size: TimeBucketSize.DAY,
           timeBucket: 'bucket',
-          isArchived: true,
+          visibility: AssetVisibility.ARCHIVE,
           userIds: [authStub.admin.user.id],
         }),
       );
@@ -77,7 +78,7 @@ describe(TimelineService.name, () => {
         sut.getTimeBucket(authStub.admin, {
           size: TimeBucketSize.DAY,
           timeBucket: 'bucket',
-          isArchived: false,
+          visibility: AssetVisibility.TIMELINE,
           userId: authStub.admin.user.id,
           withPartners: true,
         }),
@@ -85,7 +86,7 @@ describe(TimelineService.name, () => {
       expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
         size: TimeBucketSize.DAY,
         timeBucket: 'bucket',
-        isArchived: false,
+        visibility: AssetVisibility.TIMELINE,
         withPartners: true,
         userIds: [authStub.admin.user.id],
       });
@@ -120,7 +121,7 @@ describe(TimelineService.name, () => {
       const buckets = await sut.getTimeBucket(auth, {
         size: TimeBucketSize.DAY,
         timeBucket: 'bucket',
-        isArchived: true,
+        visibility: AssetVisibility.ARCHIVE,
         albumId: 'album-id',
       });
 
@@ -129,7 +130,7 @@ describe(TimelineService.name, () => {
       expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
         size: TimeBucketSize.DAY,
         timeBucket: 'bucket',
-        isArchived: true,
+        visibility: AssetVisibility.ARCHIVE,
         albumId: 'album-id',
       });
     });
@@ -154,12 +155,12 @@ describe(TimelineService.name, () => {
       );
     });
 
-    it('should throw an error if withParners is true and isArchived true or undefined', async () => {
+    it('should throw an error if withParners is true and visibility true or undefined', async () => {
       await expect(
         sut.getTimeBucket(authStub.admin, {
           size: TimeBucketSize.DAY,
           timeBucket: 'bucket',
-          isArchived: true,
+          visibility: AssetVisibility.ARCHIVE,
           withPartners: true,
           userId: authStub.admin.user.id,
         }),
@@ -169,7 +170,7 @@ describe(TimelineService.name, () => {
         sut.getTimeBucket(authStub.admin, {
           size: TimeBucketSize.DAY,
           timeBucket: 'bucket',
-          isArchived: undefined,
+          visibility: undefined,
           withPartners: true,
           userId: authStub.admin.user.id,
         }),
diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts
index 4c2332afaa..c0cd4786a8 100644
--- a/server/src/services/timeline.service.ts
+++ b/server/src/services/timeline.service.ts
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
 import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
-import { Permission } from 'src/enum';
+import { AssetVisibility, Permission } from 'src/enum';
 import { TimeBucketOptions } from 'src/repositories/asset.repository';
 import { BaseService } from 'src/services/base.service';
 import { getMyPartnerIds } from 'src/utils/asset.util';
@@ -55,7 +55,7 @@ export class TimelineService extends BaseService {
 
     if (dto.userId) {
       await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
-      if (dto.isArchived !== false) {
+      if (dto.visibility === AssetVisibility.ARCHIVE) {
         await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
       }
     }
@@ -65,7 +65,7 @@ export class TimelineService extends BaseService {
     }
 
     if (dto.withPartners) {
-      const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
+      const requestedArchived = dto.visibility === AssetVisibility.ARCHIVE || dto.visibility === undefined;
       const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
       const requestedTrash = dto.isTrashed === true;
 
diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts
index 8905f84165..0f5432da4d 100644
--- a/server/src/utils/asset.util.ts
+++ b/server/src/utils/asset.util.ts
@@ -4,7 +4,7 @@ import { AssetFile } from 'src/database';
 import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
 import { UploadFieldName } from 'src/dtos/asset-media.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { AssetFileType, AssetType, Permission } from 'src/enum';
+import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum';
 import { AuthRequest } from 'src/middleware/auth.guard';
 import { AccessRepository } from 'src/repositories/access.repository';
 import { AssetRepository } from 'src/repositories/asset.repository';
@@ -150,8 +150,8 @@ export const onBeforeLink = async (
     throw new BadRequestException('Live photo video does not belong to the user');
   }
 
-  if (motionAsset?.isVisible) {
-    await assetRepository.update({ id: livePhotoVideoId, isVisible: false });
+  if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) {
+    await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN });
     await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
   }
 };
@@ -174,9 +174,9 @@ export const onBeforeUnlink = async (
 
 export const onAfterUnlink = async (
   { asset: assetRepository, event: eventRepository }: AssetHookRepositories,
-  { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
+  { userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },
 ) => {
-  await assetRepository.update({ id: livePhotoVideoId, isVisible: true });
+  await assetRepository.update({ id: livePhotoVideoId, visibility });
   await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
 };
 
diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts
index 985605eb07..bacdf06d67 100644
--- a/server/src/utils/database.ts
+++ b/server/src/utils/database.ts
@@ -17,7 +17,7 @@ import { parse } from 'pg-connection-string';
 import postgres, { Notice } from 'postgres';
 import { columns, Exif, Person } from 'src/database';
 import { DB } from 'src/db';
-import { AssetFileType, DatabaseExtension, DatabaseSslMode } from 'src/enum';
+import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
 import { TimeBucketSize } from 'src/repositories/asset.repository';
 import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
 import { DatabaseConnectionParams, VectorExtension } from 'src/types';
@@ -155,6 +155,15 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi
 export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
 // TODO come up with a better query that only selects the fields we need
 
+export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
+  return qb.where((qb) =>
+    qb.or([
+      qb('assets.visibility', '=', AssetVisibility.TIMELINE),
+      qb('assets.visibility', '=', AssetVisibility.ARCHIVE),
+    ]),
+  );
+}
+
 export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
   return qb
     .leftJoin('exif', 'assets.id', 'exif.assetId')
@@ -280,12 +289,14 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
 /** TODO: This should only be used for search-related queries, not as a general purpose query builder */
 
 export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
-  options.isArchived ??= options.withArchived ? undefined : false;
   options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
+  const visibility = options.visibility == null ? AssetVisibility.TIMELINE : options.visibility;
+
   return kysely
     .withPlugin(joinDeduplicationPlugin)
     .selectFrom('assets')
     .selectAll('assets')
+    .where('assets.visibility', '=', visibility)
     .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
     .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
     .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
@@ -356,8 +367,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
     .$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
     .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
     .$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
-    .$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
-    .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
     .$if(options.isEncoded !== undefined, (qb) =>
       qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
     )
diff --git a/server/src/validation.ts b/server/src/validation.ts
index 29e402826d..26367aeff5 100644
--- a/server/src/validation.ts
+++ b/server/src/validation.ts
@@ -12,6 +12,7 @@ import {
   IsArray,
   IsBoolean,
   IsDate,
+  IsEnum,
   IsHexColor,
   IsNotEmpty,
   IsOptional,
@@ -29,6 +30,7 @@ import {
 import { CronJob } from 'cron';
 import { DateTime } from 'luxon';
 import sanitize from 'sanitize-filename';
+import { AssetVisibility } from 'src/enum';
 import { isIP, isIPRange } from 'validator';
 
 @Injectable()
@@ -146,6 +148,17 @@ export const ValidateDate = (options?: DateOptions) => {
   return applyDecorators(...decorators);
 };
 
+type AssetVisibilityOptions = { optional?: boolean };
+export const ValidateAssetVisibility = (options?: AssetVisibilityOptions) => {
+  const { optional } = { optional: false, ...options };
+  const decorators = [IsEnum(AssetVisibility), ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })];
+
+  if (optional) {
+    decorators.push(Optional());
+  }
+  return applyDecorators(...decorators);
+};
+
 type BooleanOptions = { optional?: boolean };
 export const ValidateBoolean = (options?: BooleanOptions) => {
   const { optional } = { optional: false, ...options };
diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts
index d1b8e7cf28..a64194361a 100644
--- a/server/test/fixtures/asset.stub.ts
+++ b/server/test/fixtures/asset.stub.ts
@@ -1,6 +1,6 @@
 import { AssetFace, AssetFile, Exif } from 'src/database';
 import { MapAsset } from 'src/dtos/asset-response.dto';
-import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
+import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
 import { StorageAsset } from 'src/types';
 import { authStub } from 'test/fixtures/auth.stub';
 import { fileStub } from 'test/fixtures/file.stub';
@@ -74,9 +74,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -90,6 +88,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   noWebpPath: Object.freeze({
@@ -111,9 +110,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -130,6 +127,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   noThumbhash: Object.freeze({
@@ -151,9 +149,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -167,6 +163,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   primaryImage: Object.freeze({
@@ -188,9 +185,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -214,6 +209,7 @@ export const assetStub = {
     isOffline: false,
     updateId: '42',
     libraryId: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   image: Object.freeze({
@@ -235,9 +231,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2025-01-01T01:02:03.456Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -257,6 +251,7 @@ export const assetStub = {
     duplicateId: null,
     isOffline: false,
     stack: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   trashed: Object.freeze({
@@ -278,9 +273,7 @@ export const assetStub = {
     deletedAt: new Date('2023-02-24T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: false,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -299,6 +292,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   trashedOffline: Object.freeze({
@@ -321,10 +315,8 @@ export const assetStub = {
     deletedAt: new Date('2023-02-24T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: false,
-    isArchived: false,
     duration: null,
     libraryId: 'library-id',
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -341,6 +333,7 @@ export const assetStub = {
     isOffline: true,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
   archived: Object.freeze({
     id: 'asset-id',
@@ -361,9 +354,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: true,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -382,6 +373,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     updateId: '42',
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   external: Object.freeze({
@@ -403,10 +395,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: true,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     libraryId: 'library-id',
@@ -423,6 +413,7 @@ export const assetStub = {
     updateId: '42',
     stackId: null,
     stack: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   image1: Object.freeze({
@@ -445,9 +436,7 @@ export const assetStub = {
     deletedAt: null,
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     isExternal: false,
@@ -464,6 +453,7 @@ export const assetStub = {
     stackId: null,
     libraryId: null,
     stack: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   imageFrom2015: Object.freeze({
@@ -485,10 +475,8 @@ export const assetStub = {
     updatedAt: new Date('2015-02-23T05:06:29.716Z'),
     localDateTime: new Date('2015-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -501,6 +489,7 @@ export const assetStub = {
     deletedAt: null,
     duplicateId: null,
     isOffline: false,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   video: Object.freeze({
@@ -523,10 +512,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -543,6 +530,7 @@ export const assetStub = {
     updateId: '42',
     libraryId: null,
     stackId: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   livePhotoMotionAsset: Object.freeze({
@@ -551,7 +539,6 @@ export const assetStub = {
     originalPath: fileStub.livePhotoMotion.originalPath,
     ownerId: authStub.user1.user.id,
     type: AssetType.VIDEO,
-    isVisible: false,
     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
     exifInfo: {
@@ -559,6 +546,7 @@ export const assetStub = {
       timeZone: `America/New_York`,
     },
     libraryId: null,
+    visibility: AssetVisibility.HIDDEN,
   } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
 
   livePhotoStillAsset: Object.freeze({
@@ -568,7 +556,6 @@ export const assetStub = {
     ownerId: authStub.user1.user.id,
     type: AssetType.IMAGE,
     livePhotoVideoId: 'live-photo-motion-asset',
-    isVisible: true,
     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
     exifInfo: {
@@ -577,6 +564,7 @@ export const assetStub = {
     },
     files,
     faces: [] as AssetFace[],
+    visibility: AssetVisibility.TIMELINE,
   } as MapAsset & { faces: AssetFace[] }),
 
   livePhotoWithOriginalFileName: Object.freeze({
@@ -587,7 +575,6 @@ export const assetStub = {
     ownerId: authStub.user1.user.id,
     type: AssetType.IMAGE,
     livePhotoVideoId: 'live-photo-motion-asset',
-    isVisible: true,
     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
     exifInfo: {
@@ -596,6 +583,7 @@ export const assetStub = {
     },
     libraryId: null,
     faces: [] as AssetFace[],
+    visibility: AssetVisibility.TIMELINE,
   } as MapAsset & { faces: AssetFace[] }),
 
   withLocation: Object.freeze({
@@ -618,10 +606,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-22T05:06:29.716Z'),
     localDateTime: new Date('2020-12-31T23:59:00.000Z'),
     isFavorite: false,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     updateId: 'foo',
@@ -642,6 +628,7 @@ export const assetStub = {
     duplicateId: null,
     isOffline: false,
     tags: [],
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   sidecar: Object.freeze({
@@ -663,10 +650,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -679,6 +664,7 @@ export const assetStub = {
     updateId: 'foo',
     libraryId: null,
     stackId: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   sidecarWithoutExt: Object.freeze({
@@ -700,10 +686,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -713,6 +697,7 @@ export const assetStub = {
     deletedAt: null,
     duplicateId: null,
     isOffline: false,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   hasEncodedVideo: Object.freeze({
@@ -735,10 +720,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: false,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     sharedLinks: [],
@@ -754,6 +737,7 @@ export const assetStub = {
     libraryId: null,
     stackId: null,
     stack: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   hasFileExtension: Object.freeze({
@@ -775,10 +759,8 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     isExternal: true,
     duration: null,
-    isVisible: true,
     livePhotoVideo: null,
     livePhotoVideoId: null,
     libraryId: 'library-id',
@@ -792,6 +774,7 @@ export const assetStub = {
     } as Exif,
     duplicateId: null,
     isOffline: false,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   imageDng: Object.freeze({
@@ -813,9 +796,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -834,6 +815,7 @@ export const assetStub = {
     updateId: '42',
     libraryId: null,
     stackId: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 
   imageHif: Object.freeze({
@@ -855,9 +837,7 @@ export const assetStub = {
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
     localDateTime: new Date('2023-02-23T05:06:29.716Z'),
     isFavorite: true,
-    isArchived: false,
     duration: null,
-    isVisible: true,
     isExternal: false,
     livePhotoVideo: null,
     livePhotoVideoId: null,
@@ -876,5 +856,6 @@ export const assetStub = {
     updateId: '42',
     libraryId: null,
     stackId: null,
+    visibility: AssetVisibility.TIMELINE,
   }),
 };
diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts
index a4d83863c7..fc4b74ba2d 100644
--- a/server/test/fixtures/shared-link.stub.ts
+++ b/server/test/fixtures/shared-link.stub.ts
@@ -4,7 +4,7 @@ import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto';
 import { ExifResponseDto } from 'src/dtos/exif.dto';
 import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
 import { mapUser } from 'src/dtos/user.dto';
-import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
+import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
 import { assetStub } from 'test/fixtures/asset.stub';
 import { authStub } from 'test/fixtures/auth.stub';
 import { userStub } from 'test/fixtures/user.stub';
@@ -206,7 +206,6 @@ export const sharedLinkStub = {
           thumbhash: null,
           encodedVideoPath: '',
           duration: null,
-          isVisible: true,
           livePhotoVideo: null,
           livePhotoVideoId: null,
           originalFileName: 'asset_1.jpeg',
@@ -251,6 +250,7 @@ export const sharedLinkStub = {
           updateId: '42',
           libraryId: null,
           stackId: null,
+          visibility: AssetVisibility.TIMELINE,
         },
       ],
     },
diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts
index 89b1921819..6f4f46c075 100644
--- a/server/test/medium.factory.ts
+++ b/server/test/medium.factory.ts
@@ -5,7 +5,7 @@ import { createHash, randomBytes } from 'node:crypto';
 import { Writable } from 'node:stream';
 import { AssetFace } from 'src/database';
 import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
-import { AssetType, SourceType } from 'src/enum';
+import { AssetType, AssetVisibility, SourceType } from 'src/enum';
 import { ActivityRepository } from 'src/repositories/activity.repository';
 import { AlbumRepository } from 'src/repositories/album.repository';
 import { AssetJobRepository } from 'src/repositories/asset-job.repository';
@@ -227,16 +227,37 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
 
     case 'database': {
       return automock(DatabaseRepository, {
-        args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
+        args: [
+          undefined,
+          {
+            setContext: () => {},
+          },
+          { getEnv: () => ({ database: { vectorExtension: '' } }) },
+        ],
       });
     }
 
     case 'email': {
-      return automock(EmailRepository, { args: [{ setContext: () => {} }] });
+      return automock(EmailRepository, {
+        args: [
+          {
+            setContext: () => {},
+          },
+        ],
+      });
     }
 
     case 'job': {
-      return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] });
+      return automock(JobRepository, {
+        args: [
+          undefined,
+          undefined,
+          undefined,
+          {
+            setContext: () => {},
+          },
+        ],
+      });
     }
 
     case 'logger': {
@@ -345,11 +366,11 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
     type: AssetType.IMAGE,
     originalPath: '/path/to/something.jpg',
     ownerId: '@immich.cloud',
-    isVisible: true,
     isFavorite: false,
     fileCreatedAt: now,
     fileModifiedAt: now,
     localDateTime: now,
+    visibility: AssetVisibility.TIMELINE,
   };
 
   return {
diff --git a/server/test/medium/specs/services/sync.service.spec.ts b/server/test/medium/specs/services/sync.service.spec.ts
index 98df296cbf..67cfeafdbf 100644
--- a/server/test/medium/specs/services/sync.service.spec.ts
+++ b/server/test/medium/specs/services/sync.service.spec.ts
@@ -456,9 +456,9 @@ describe(SyncService.name, () => {
               fileCreatedAt: asset.fileCreatedAt,
               fileModifiedAt: asset.fileModifiedAt,
               isFavorite: asset.isFavorite,
-              isVisible: asset.isVisible,
               localDateTime: asset.localDateTime,
               type: asset.type,
+              visibility: asset.visibility,
             },
             type: 'AssetV1',
           },
@@ -573,9 +573,9 @@ describe(SyncService.name, () => {
               fileCreatedAt: date,
               fileModifiedAt: date,
               isFavorite: false,
-              isVisible: true,
               localDateTime: date,
               type: asset.type,
+              visibility: asset.visibility,
             },
             type: SyncEntityType.PartnerAssetV1,
           },
diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts
index 81ada65b68..94ae3b74aa 100644
--- a/server/test/small.factory.ts
+++ b/server/test/small.factory.ts
@@ -15,7 +15,7 @@ import {
 } from 'src/database';
 import { MapAsset } from 'src/dtos/asset-response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum';
+import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum';
 import { OnThisDayData } from 'src/types';
 
 export const newUuid = () => randomUUID() as string;
@@ -202,11 +202,9 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
   encodedVideoPath: null,
   fileCreatedAt: newDate(),
   fileModifiedAt: newDate(),
-  isArchived: false,
   isExternal: false,
   isFavorite: false,
   isOffline: false,
-  isVisible: true,
   libraryId: null,
   livePhotoVideoId: null,
   localDateTime: newDate(),
@@ -217,6 +215,7 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
   stackId: null,
   thumbhash: null,
   type: AssetType.IMAGE,
+  visibility: AssetVisibility.TIMELINE,
   ...asset,
 });
 
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
index 576c1af540..d5c9b02a2d 100644
--- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
@@ -1,8 +1,8 @@
 <script lang="ts" module>
-  import type { SearchLocationFilter } from './search-location-section.svelte';
-  import type { SearchDisplayFilters } from './search-display-section.svelte';
-  import type { SearchDateFilter } from './search-date-section.svelte';
   import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
+  import type { SearchDateFilter } from './search-date-section.svelte';
+  import type { SearchDisplayFilters } from './search-display-section.svelte';
+  import type { SearchLocationFilter } from './search-location-section.svelte';
 
   export type SearchFilter = {
     query: string;
@@ -19,24 +19,24 @@
 </script>
 
 <script lang="ts">
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import { preferences } from '$lib/stores/user.store';
+  import { parseUtcDate } from '$lib/utils/date-time';
+  import { generateId } from '$lib/utils/generate-id';
+  import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
   import { Button } from '@immich/ui';
-  import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
-  import SearchPeopleSection from './search-people-section.svelte';
-  import SearchTagsSection from './search-tags-section.svelte';
-  import SearchLocationSection from './search-location-section.svelte';
+  import { mdiTune } from '@mdi/js';
+  import { t } from 'svelte-i18n';
+  import { SvelteSet } from 'svelte/reactivity';
   import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
   import SearchDateSection from './search-date-section.svelte';
-  import SearchMediaSection from './search-media-section.svelte';
-  import SearchRatingsSection from './search-ratings-section.svelte';
-  import { parseUtcDate } from '$lib/utils/date-time';
   import SearchDisplaySection from './search-display-section.svelte';
+  import SearchLocationSection from './search-location-section.svelte';
+  import SearchMediaSection from './search-media-section.svelte';
+  import SearchPeopleSection from './search-people-section.svelte';
+  import SearchRatingsSection from './search-ratings-section.svelte';
+  import SearchTagsSection from './search-tags-section.svelte';
   import SearchTextSection from './search-text-section.svelte';
-  import { t } from 'svelte-i18n';
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import { mdiTune } from '@mdi/js';
-  import { generateId } from '$lib/utils/generate-id';
-  import { SvelteSet } from 'svelte/reactivity';
-  import { preferences } from '$lib/stores/user.store';
 
   interface Props {
     searchQuery: MetadataSearchDto | SmartSearchDto;
@@ -83,7 +83,7 @@
       takenBefore: searchQuery.takenBefore ? toStartOfDayDate(searchQuery.takenBefore) : undefined,
     },
     display: {
-      isArchive: searchQuery.isArchived,
+      isArchive: searchQuery.visibility === AssetVisibility.Archive,
       isFavorite: searchQuery.isFavorite,
       isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined,
     },
@@ -132,7 +132,7 @@
       model: filter.camera.model,
       takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined,
       takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('day').toISO() || undefined,
-      isArchived: filter.display.isArchive || undefined,
+      visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined,
       isFavorite: filter.display.isFavorite || undefined,
       isNotInAlbum: filter.display.isNotInAlbum || undefined,
       personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined,
diff --git a/web/src/lib/components/user-settings-page/user-usage-statistic.svelte b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte
index 6805ce80e5..c87cdf8cf6 100644
--- a/web/src/lib/components/user-settings-page/user-usage-statistic.svelte
+++ b/web/src/lib/components/user-settings-page/user-usage-statistic.svelte
@@ -1,6 +1,7 @@
 <script lang="ts">
   import { locale } from '$lib/stores/preferences.store';
   import {
+    AssetVisibility,
     getAlbumStatistics,
     getAssetStatistics,
     type AlbumStatisticsResponseDto,
@@ -41,9 +42,9 @@
 
   const getUsage = async () => {
     [timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([
-      getAssetStatistics({ isArchived: false }),
+      getAssetStatistics({ visibility: AssetVisibility.Timeline }),
       getAssetStatistics({ isFavorite: true }),
-      getAssetStatistics({ isArchived: true }),
+      getAssetStatistics({ visibility: AssetVisibility.Archive }),
       getAssetStatistics({ isTrashed: true }),
       getAlbumStatistics(),
     ]);
diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts
index b4b4a4ade2..dda45d74d2 100644
--- a/web/src/lib/stores/assets-store.svelte.ts
+++ b/web/src/lib/stores/assets-store.svelte.ts
@@ -10,6 +10,7 @@ import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-uti
 import { TUNABLES } from '$lib/utils/tunables';
 import {
   AssetOrder,
+  AssetVisibility,
   getAssetInfo,
   getTimeBucket,
   getTimeBuckets,
@@ -1375,7 +1376,7 @@ export class AssetStore {
 
   isExcluded(asset: AssetResponseDto) {
     return (
-      isMismatched(this.#options.isArchived, asset.isArchived) ||
+      isMismatched(this.#options.visibility === AssetVisibility.Archive, asset.isArchived) ||
       isMismatched(this.#options.isFavorite, asset.isFavorite) ||
       isMismatched(this.#options.isTrashed, asset.isTrashed)
     );
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 254b90f08d..c23b9b432c 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -1,7 +1,7 @@
 import { goto } from '$app/navigation';
 import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
 import type { InterpolationValues } from '$lib/components/i18n/format-message';
-import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
+import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
 import { AppRoute } from '$lib/constants';
 import { authManager } from '$lib/managers/auth-manager.svelte';
 import { downloadManager } from '$lib/managers/download-manager.svelte';
@@ -15,6 +15,7 @@ import { getFormatter } from '$lib/utils/i18n';
 import { navigate } from '$lib/utils/navigation';
 import {
   addAssetsToAlbum as addAssets,
+  AssetVisibility,
   createStack,
   deleteAssets,
   deleteStacks,
@@ -507,7 +508,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
     const data = await updateAsset({
       id: asset.id,
       updateAssetDto: {
-        isArchived: !asset.isArchived,
+        visibility: asset.isArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
       },
     });
 
@@ -531,7 +532,9 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
 
   try {
     if (ids.length > 0) {
-      await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
+      await updateAssets({
+        assetBulkUpdateDto: { ids, visibility: isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline },
+      });
     }
 
     for (const asset of assets) {
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index d35e8fefd4..c24c8c8399 100644
--- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -372,7 +372,10 @@
     if (viewMode === AlbumPageViewMode.VIEW) {
       void assetStore.updateOptions({ albumId, order: albumOrder });
     } else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
-      void assetStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId });
+      void assetStore.updateOptions({
+        withPartners: true,
+        timelineAlbumId: albumId,
+      });
     }
   });
 
@@ -385,9 +388,6 @@
     activityManager.reset();
     assetStore.destroy();
   });
-  // let timelineStore = new AssetStore();
-  // $effect(() => void timelineStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId }));
-  // onDestroy(() => timelineStore.destroy());
 
   let isOwned = $derived($user.id == album.ownerId);
 
diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 86cfefff77..647b92c3f8 100644
--- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -8,17 +8,18 @@
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
 
-  import type { PageData } from './$types';
-  import { mdiPlus, mdiDotsVertical } from '@mdi/js';
-  import { t } from 'svelte-i18n';
-  import { onDestroy } from 'svelte';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { AssetStore } from '$lib/stores/assets-store.svelte';
+  import { AssetVisibility } from '@immich/sdk';
+  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
+  import { onDestroy } from 'svelte';
+  import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -26,7 +27,7 @@
 
   let { data }: Props = $props();
   const assetStore = new AssetStore();
-  void assetStore.updateOptions({ isArchived: true });
+  void assetStore.updateOptions({ visibility: AssetVisibility.Archive });
   onDestroy(() => assetStore.destroy());
 
   const assetInteraction = new AssetInteraction();
diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 120281b07e..2ed4b0ada7 100644
--- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -9,19 +9,19 @@
   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 ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import { AssetAction } from '$lib/constants';
-  import { AssetStore } from '$lib/stores/assets-store.svelte';
-  import type { PageData } from './$types';
-  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
-  import { t } from 'svelte-i18n';
-  import { onDestroy } from '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 { AssetStore } from '$lib/stores/assets-store.svelte';
+  import { preferences } from '$lib/stores/user.store';
+  import { mdiDotsVertical, mdiPlus } from '@mdi/js';
+  import { onDestroy } from 'svelte';
+  import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
index fe8c45f2f5..2f916d732c 100644
--- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -4,16 +4,17 @@
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import { AppRoute } from '$lib/constants';
-  import { AssetStore } from '$lib/stores/assets-store.svelte';
-  import { onDestroy } from 'svelte';
-  import type { PageData } from './$types';
-  import { mdiPlus, mdiArrowLeft } from '@mdi/js';
-  import { t } from 'svelte-i18n';
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+  import { AssetStore } from '$lib/stores/assets-store.svelte';
+  import { AssetVisibility } from '@immich/sdk';
+  import { mdiArrowLeft, mdiPlus } from '@mdi/js';
+  import { onDestroy } from 'svelte';
+  import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -22,7 +23,14 @@
   let { data }: Props = $props();
 
   const assetStore = new AssetStore();
-  $effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
+  $effect(
+    () =>
+      void assetStore.updateOptions({
+        userId: data.partner.id,
+        visibility: AssetVisibility.Timeline,
+        withStacked: true,
+      }),
+  );
   onDestroy(() => assetStore.destroy());
   const assetInteraction = new AssetInteraction();
 
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 4c58f52265..6b260cf9c5 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
@@ -34,12 +34,14 @@
   import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
   import { AssetStore } from '$lib/stores/assets-store.svelte';
+  import { locale } from '$lib/stores/preferences.store';
   import { preferences } from '$lib/stores/user.store';
   import { websocketEvents } from '$lib/stores/websocket';
   import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
   import { isExternalUrl } from '$lib/utils/navigation';
   import {
+    AssetVisibility,
     getPersonStatistics,
     mergePerson,
     searchPerson,
@@ -59,11 +61,10 @@
     mdiHeartOutline,
     mdiPlus,
   } from '@mdi/js';
+  import { DateTime } from 'luxon';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
-  import { locale } from '$lib/stores/preferences.store';
-  import { DateTime } from 'luxon';
 
   interface Props {
     data: PageData;
@@ -75,7 +76,7 @@
   let { isViewing: showAssetViewer } = assetViewingStore;
 
   const assetStore = new AssetStore();
-  $effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
+  $effect(() => void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
   onDestroy(() => assetStore.destroy());
 
   const assetInteraction = new AssetInteraction();
diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
index 34498a60a3..e9827c06f5 100644
--- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte
@@ -32,14 +32,14 @@
     type OnUnlink,
   } from '$lib/utils/actions';
   import { openFileUploadDialog } from '$lib/utils/file-uploader';
-  import { AssetTypeEnum } from '@immich/sdk';
+  import { AssetTypeEnum, AssetVisibility } from '@immich/sdk';
   import { mdiDotsVertical, mdiPlus } from '@mdi/js';
   import { onDestroy } from 'svelte';
   import { t } from 'svelte-i18n';
 
   let { isViewing: showAssetViewer } = assetViewingStore;
   const assetStore = new AssetStore();
-  void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
+  void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
   onDestroy(() => assetStore.destroy());
 
   const assetInteraction = new AssetInteraction();
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 9d427e1ea7..dc03a2ae70 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,25 +1,38 @@
 <script lang="ts">
   import { afterNavigate, goto } from '$app/navigation';
   import { page } from '$app/state';
+  import { shortcut } from '$lib/actions/shortcut';
+  import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
+  import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
   import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
   import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
+  import TagAction from '$lib/components/photos-page/actions/tag-action.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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
-  import { cancelMultiselect } from '$lib/utils/asset-utils';
+  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import { shortcut } from '$lib/actions/shortcut';
+  import type { Viewport } from '$lib/stores/assets-store.svelte';
+  import { lang, locale } from '$lib/stores/preferences.store';
+  import { featureFlags } from '$lib/stores/server-config.store';
+  import { preferences } from '$lib/stores/user.store';
+  import { handlePromiseError } from '$lib/utils';
+  import { cancelMultiselect } from '$lib/utils/asset-utils';
+  import { parseUtcDate } from '$lib/utils/date-time';
+  import { handleError } from '$lib/utils/handle-error';
+  import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
   import {
     type AlbumResponseDto,
     type AssetResponseDto,
@@ -31,21 +44,8 @@
     type SmartSearchDto,
   } from '@immich/sdk';
   import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
-  import type { Viewport } from '$lib/stores/assets-store.svelte';
-  import { lang, locale } from '$lib/stores/preferences.store';
-  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
-  import { handlePromiseError } from '$lib/utils';
-  import { parseUtcDate } from '$lib/utils/date-time';
-  import { featureFlags } from '$lib/stores/server-config.store';
-  import { handleError } from '$lib/utils/handle-error';
-  import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
-  import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
-  import { t } from 'svelte-i18n';
   import { tick } from 'svelte';
-  import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.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 { t } from 'svelte-i18n';
 
   const MAX_ASSET_COUNT = 5000;
   let { isViewing: showAssetViewer } = assetViewingStore;
@@ -186,7 +186,7 @@
     const keyMap: Partial<Record<keyof SearchTerms, string>> = {
       takenAfter: $t('start_date'),
       takenBefore: $t('end_date'),
-      isArchived: $t('in_archive'),
+      visibility: $t('in_archive'),
       isFavorite: $t('favorite'),
       isNotInAlbum: $t('not_in_any_album'),
       type: $t('media_type'),
@@ -313,7 +313,7 @@
       <div class="flex place-content-center place-items-center text-xs">
         <div
           class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
-          {value === true ? 'rounded-full' : 'roudned-s-full'}"
+          {value === true ? 'rounded-full' : 'rounded-s-full'}"
         >
           {getHumanReadableSearchKey(key as keyof SearchTerms)}
         </div>