diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts
index 4f386a51ef..e20a0c658d 100644
--- a/server/src/cores/storage.core.ts
+++ b/server/src/cores/storage.core.ts
@@ -6,6 +6,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
 import { PersonEntity } from 'src/entities/person.entity';
+import { AssetFileType } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
@@ -13,6 +14,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 
 export enum StorageFolder {
   ENCODED_VIDEO = 'encoded-video',
@@ -130,12 +132,14 @@ export class StorageCore {
   }
 
   async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
-    const { id: entityId, previewPath, thumbnailPath } = asset;
+    const { id: entityId, files } = asset;
+    const { thumbnailFile, previewFile } = getAssetFiles(files);
+    const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile;
     return this.moveFile({
       entityId,
       pathType,
-      oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath,
-      newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format),
+      oldPath: oldFile?.path || null,
+      newPath: StorageCore.getImagePath(asset, pathType, format),
     });
   }
 
@@ -285,10 +289,10 @@ export class StorageCore {
         return this.assetRepository.update({ id, originalPath: newPath });
       }
       case AssetPathType.PREVIEW: {
-        return this.assetRepository.update({ id, previewPath: newPath });
+        return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath });
       }
       case AssetPathType.THUMBNAIL: {
-        return this.assetRepository.update({ id, thumbnailPath: newPath });
+        return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath });
       }
       case AssetPathType.ENCODED_VIDEO: {
         return this.assetRepository.update({ id, encodedVideoPath: newPath });
diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts
index 6ed1125253..332f258d49 100644
--- a/server/src/dtos/asset-response.dto.ts
+++ b/server/src/dtos/asset-response.dto.ts
@@ -14,6 +14,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { SmartInfoEntity } from 'src/entities/smart-info.entity';
 import { AssetType } from 'src/enum';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { mimeTypes } from 'src/utils/mime-types';
 
 export class SanitizedAssetResponseDto {
@@ -111,7 +112,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
       originalMimeType: mimeTypes.lookup(entity.originalFileName),
       thumbhash: entity.thumbhash?.toString('base64') ?? null,
       localDateTime: entity.localDateTime,
-      resized: !!entity.previewPath,
+      resized: !!getAssetFiles(entity.files).previewFile,
       duration: entity.duration ?? '0:00:00.00000',
       livePhotoVideoId: entity.livePhotoVideoId,
       hasMetadata: false,
@@ -130,7 +131,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
     originalPath: entity.originalPath,
     originalFileName: entity.originalFileName,
     originalMimeType: mimeTypes.lookup(entity.originalFileName),
-    resized: !!entity.previewPath,
+    resized: !!getAssetFiles(entity.files).previewFile,
     thumbhash: entity.thumbhash?.toString('base64') ?? null,
     fileCreatedAt: entity.fileCreatedAt,
     fileModifiedAt: entity.fileModifiedAt,
diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts
new file mode 100644
index 0000000000..a8a6ddfee1
--- /dev/null
+++ b/server/src/entities/asset-files.entity.ts
@@ -0,0 +1,38 @@
+import { AssetEntity } from 'src/entities/asset.entity';
+import { AssetFileType } from 'src/enum';
+import {
+  Column,
+  CreateDateColumn,
+  Entity,
+  Index,
+  ManyToOne,
+  PrimaryGeneratedColumn,
+  Unique,
+  UpdateDateColumn,
+} from 'typeorm';
+
+@Unique('UQ_assetId_type', ['assetId', 'type'])
+@Entity('asset_files')
+export class AssetFileEntity {
+  @PrimaryGeneratedColumn('uuid')
+  id!: string;
+
+  @Index('IDX_asset_files_assetId')
+  @Column()
+  assetId!: string;
+
+  @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
+  asset?: AssetEntity;
+
+  @CreateDateColumn({ type: 'timestamptz' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ type: 'timestamptz' })
+  updatedAt!: Date;
+
+  @Column()
+  type!: AssetFileType;
+
+  @Column()
+  path!: string;
+}
diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts
index f4ea5eafdd..9ebf9364d1 100644
--- a/server/src/entities/asset.entity.ts
+++ b/server/src/entities/asset.entity.ts
@@ -1,5 +1,6 @@
 import { AlbumEntity } from 'src/entities/album.entity';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
 import { LibraryEntity } from 'src/entities/library.entity';
@@ -72,11 +73,8 @@ export class AssetEntity {
   @Column()
   originalPath!: string;
 
-  @Column({ type: 'varchar', nullable: true })
-  previewPath!: string | null;
-
-  @Column({ type: 'varchar', nullable: true, default: '' })
-  thumbnailPath!: string | null;
+  @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset)
+  files!: AssetFileEntity[];
 
   @Column({ type: 'bytea', nullable: true })
   thumbhash!: Buffer | null;
diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts
index 148e264095..0b7ca8c3bd 100644
--- a/server/src/entities/index.ts
+++ b/server/src/entities/index.ts
@@ -3,6 +3,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity';
 import { AlbumEntity } from 'src/entities/album.entity';
 import { APIKeyEntity } from 'src/entities/api-key.entity';
 import { AssetFaceEntity } from 'src/entities/asset-face.entity';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AuditEntity } from 'src/entities/audit.entity';
@@ -32,6 +33,7 @@ export const entities = [
   APIKeyEntity,
   AssetEntity,
   AssetFaceEntity,
+  AssetFileEntity,
   AssetJobStatusEntity,
   AuditEntity,
   ExifEntity,
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 4a81d54218..64cb1f118a 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -5,6 +5,11 @@ export enum AssetType {
   OTHER = 'OTHER',
 }
 
+export enum AssetFileType {
+  PREVIEW = 'preview',
+  THUMBNAIL = 'thumbnail',
+}
+
 export enum AlbumUserRole {
   EDITOR = 'editor',
   VIEWER = 'viewer',
diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts
index aca45f3dc7..6dd81edaef 100644
--- a/server/src/interfaces/asset.interface.ts
+++ b/server/src/interfaces/asset.interface.ts
@@ -1,7 +1,7 @@
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetOrder, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
 import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
 import { Paginated, PaginationOptions } from 'src/utils/pagination';
 import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
@@ -191,4 +191,5 @@ export interface IAssetRepository {
   getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>;
   getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
   getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
+  upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise<void>;
 }
diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts
new file mode 100644
index 0000000000..1ed4945749
--- /dev/null
+++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts
@@ -0,0 +1,34 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddAssetFilesTable1724101822106 implements MigrationInterface {
+    name = 'AddAssetFilesTable1724101822106'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type"), CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId") `);
+        await queryRunner.query(`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
+
+        // preview path migration
+        await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'preview', "previewPath" FROM "assets" WHERE "previewPath" IS NOT NULL AND "previewPath" != ''`);
+        await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "previewPath"`);
+
+        // thumbnail path migration
+        await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'thumbnail', "thumbnailPath" FROM "assets" WHERE "thumbnailPath" IS NOT NULL AND "thumbnailPath" != ''`);
+        await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "thumbnailPath"`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        // undo preview path migration
+        await queryRunner.query(`ALTER TABLE "assets" ADD "previewPath" character varying`);
+        await queryRunner.query(`UPDATE "assets" SET "previewPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'preview'`);
+
+        // undo thumbnail path migration
+        await queryRunner.query(`ALTER TABLE "assets" ADD "thumbnailPath" character varying DEFAULT ''`);
+        await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`);
+
+        await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`);
+        await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`);
+        await queryRunner.query(`DROP TABLE "asset_files"`);
+    }
+
+}
diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql
index 98fb1d6999..c9bd8083bb 100644
--- a/server/src/queries/asset.repository.sql
+++ b/server/src/queries/asset.repository.sql
@@ -9,8 +9,6 @@ SELECT
   "entity"."deviceId" AS "entity_deviceId",
   "entity"."type" AS "entity_type",
   "entity"."originalPath" AS "entity_originalPath",
-  "entity"."previewPath" AS "entity_previewPath",
-  "entity"."thumbnailPath" AS "entity_thumbnailPath",
   "entity"."thumbhash" AS "entity_thumbhash",
   "entity"."encodedVideoPath" AS "entity_encodedVideoPath",
   "entity"."createdAt" AS "entity_createdAt",
@@ -59,16 +57,22 @@ SELECT
   "exifInfo"."colorspace" AS "exifInfo_colorspace",
   "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
   "exifInfo"."rating" AS "exifInfo_rating",
-  "exifInfo"."fps" AS "exifInfo_fps"
+  "exifInfo"."fps" AS "exifInfo_fps",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path"
 FROM
   "assets" "entity"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id"
 WHERE
   (
     "entity"."ownerId" IN ($1)
     AND "entity"."isVisible" = true
     AND "entity"."isArchived" = false
-    AND "entity"."previewPath" IS NOT NULL
     AND EXTRACT(
       DAY
       FROM
@@ -93,8 +97,6 @@ SELECT
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-  "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-  "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
   "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -129,8 +131,6 @@ SELECT
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-  "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-  "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
   "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -216,8 +216,6 @@ SELECT
   "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
   "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
   "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
-  "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath",
-  "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath",
   "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
   "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
   "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt",
@@ -237,7 +235,13 @@ SELECT
   "bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName",
   "bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath",
   "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId",
-  "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId"
+  "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId",
+  "AssetEntity__AssetEntity_files"."id" AS "AssetEntity__AssetEntity_files_id",
+  "AssetEntity__AssetEntity_files"."assetId" AS "AssetEntity__AssetEntity_files_assetId",
+  "AssetEntity__AssetEntity_files"."createdAt" AS "AssetEntity__AssetEntity_files_createdAt",
+  "AssetEntity__AssetEntity_files"."updatedAt" AS "AssetEntity__AssetEntity_files_updatedAt",
+  "AssetEntity__AssetEntity_files"."type" AS "AssetEntity__AssetEntity_files_type",
+  "AssetEntity__AssetEntity_files"."path" AS "AssetEntity__AssetEntity_files_path"
 FROM
   "assets" "AssetEntity"
   LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
@@ -248,6 +252,7 @@ FROM
   LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId"
   LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId"
   LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id"
+  LEFT JOIN "asset_files" "AssetEntity__AssetEntity_files" ON "AssetEntity__AssetEntity_files"."assetId" = "AssetEntity"."id"
 WHERE
   (("AssetEntity"."id" IN ($1)))
 
@@ -298,8 +303,6 @@ FROM
       "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
       "AssetEntity"."type" AS "AssetEntity_type",
       "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-      "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-      "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
       "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
       "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
       "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -397,8 +400,6 @@ SELECT
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-  "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-  "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
   "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -452,8 +453,6 @@ SELECT
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-  "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-  "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
   "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -525,8 +524,6 @@ SELECT
   "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
   "AssetEntity"."type" AS "AssetEntity_type",
   "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-  "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-  "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
   "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -581,8 +578,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -603,6 +598,12 @@ SELECT
   "asset"."sidecarPath" AS "asset_sidecarPath",
   "asset"."stackId" AS "asset_stackId",
   "asset"."duplicateId" AS "asset_duplicateId",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path",
   "exifInfo"."assetId" AS "exifInfo_assetId",
   "exifInfo"."description" AS "exifInfo_description",
   "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@@ -642,8 +643,6 @@ SELECT
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-  "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
-  "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
   "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@@ -666,6 +665,7 @@ SELECT
   "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
   LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@@ -692,6 +692,7 @@ SELECT
   )::timestamptz AS "timeBucket"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
   LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@@ -723,8 +724,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -745,6 +744,12 @@ SELECT
   "asset"."sidecarPath" AS "asset_sidecarPath",
   "asset"."stackId" AS "asset_stackId",
   "asset"."duplicateId" AS "asset_duplicateId",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path",
   "exifInfo"."assetId" AS "exifInfo_assetId",
   "exifInfo"."description" AS "exifInfo_description",
   "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@@ -784,8 +789,6 @@ SELECT
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-  "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
-  "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
   "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@@ -808,6 +811,7 @@ SELECT
   "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
   LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@@ -841,8 +845,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -863,6 +865,12 @@ SELECT
   "asset"."sidecarPath" AS "asset_sidecarPath",
   "asset"."stackId" AS "asset_stackId",
   "asset"."duplicateId" AS "asset_duplicateId",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path",
   "exifInfo"."assetId" AS "exifInfo_assetId",
   "exifInfo"."description" AS "exifInfo_description",
   "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@@ -902,8 +910,6 @@ SELECT
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-  "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
-  "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
   "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@@ -926,6 +932,7 @@ SELECT
   "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
   LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
@@ -957,6 +964,7 @@ SELECT DISTINCT
   c.city AS "value"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   INNER JOIN "exif" "e" ON "asset"."id" = e."assetId"
   INNER JOIN "cities" "c" ON c.city = "e"."city"
 WHERE
@@ -987,6 +995,7 @@ SELECT DISTINCT
   unnest("si"."tags") AS "value"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId"
   INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag]
 WHERE
@@ -1009,8 +1018,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -1031,6 +1038,12 @@ SELECT
   "asset"."sidecarPath" AS "asset_sidecarPath",
   "asset"."stackId" AS "asset_stackId",
   "asset"."duplicateId" AS "asset_duplicateId",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path",
   "exifInfo"."assetId" AS "exifInfo_assetId",
   "exifInfo"."description" AS "exifInfo_description",
   "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@@ -1065,6 +1078,7 @@ SELECT
   "stack"."primaryAssetId" AS "stack_primaryAssetId"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
 WHERE
@@ -1086,8 +1100,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -1108,6 +1120,12 @@ SELECT
   "asset"."sidecarPath" AS "asset_sidecarPath",
   "asset"."stackId" AS "asset_stackId",
   "asset"."duplicateId" AS "asset_duplicateId",
+  "files"."id" AS "files_id",
+  "files"."assetId" AS "files_assetId",
+  "files"."createdAt" AS "files_createdAt",
+  "files"."updatedAt" AS "files_updatedAt",
+  "files"."type" AS "files_type",
+  "files"."path" AS "files_path",
   "exifInfo"."assetId" AS "exifInfo_assetId",
   "exifInfo"."description" AS "exifInfo_description",
   "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
@@ -1142,9 +1160,34 @@ SELECT
   "stack"."primaryAssetId" AS "stack_primaryAssetId"
 FROM
   "assets" "asset"
+  LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id"
   LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
   LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
 WHERE
   "asset"."isVisible" = true
   AND "asset"."ownerId" IN ($1)
   AND "asset"."updatedAt" > $2
+
+-- AssetRepository.upsertFile
+INSERT INTO
+  "asset_files" (
+    "id",
+    "assetId",
+    "createdAt",
+    "updatedAt",
+    "type",
+    "path"
+  )
+VALUES
+  (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3)
+ON CONFLICT ("assetId", "type") DO
+UPDATE
+SET
+  "assetId" = EXCLUDED."assetId",
+  "type" = EXCLUDED."type",
+  "path" = EXCLUDED."path",
+  "updatedAt" = DEFAULT
+RETURNING
+  "id",
+  "createdAt",
+  "updatedAt"
diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql
index 9b20b964d8..9c94232d20 100644
--- a/server/src/queries/person.repository.sql
+++ b/server/src/queries/person.repository.sql
@@ -157,8 +157,6 @@ FROM
       "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
       "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
       "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
-      "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath",
-      "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath",
       "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
       "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
       "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
@@ -255,8 +253,6 @@ FROM
       "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
       "AssetEntity"."type" AS "AssetEntity_type",
       "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
-      "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
-      "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
       "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
       "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
       "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@@ -386,8 +382,6 @@ SELECT
   "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
   "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
   "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
-  "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath",
-  "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath",
   "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
   "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
   "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql
index 390aedaf35..e9e94400ad 100644
--- a/server/src/queries/search.repository.sql
+++ b/server/src/queries/search.repository.sql
@@ -14,8 +14,6 @@ FROM
       "asset"."deviceId" AS "asset_deviceId",
       "asset"."type" AS "asset_type",
       "asset"."originalPath" AS "asset_originalPath",
-      "asset"."previewPath" AS "asset_previewPath",
-      "asset"."thumbnailPath" AS "asset_thumbnailPath",
       "asset"."thumbhash" AS "asset_thumbhash",
       "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
       "asset"."createdAt" AS "asset_createdAt",
@@ -46,8 +44,6 @@ FROM
       "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
       "stackedAssets"."type" AS "stackedAssets_type",
       "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-      "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
-      "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
       "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
       "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
       "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@@ -111,8 +107,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
@@ -143,8 +137,6 @@ SELECT
   "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
   "stackedAssets"."type" AS "stackedAssets_type",
   "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
-  "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
-  "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
   "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
   "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
   "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@@ -353,8 +345,6 @@ SELECT
   "asset"."deviceId" AS "asset_deviceId",
   "asset"."type" AS "asset_type",
   "asset"."originalPath" AS "asset_originalPath",
-  "asset"."previewPath" AS "asset_previewPath",
-  "asset"."thumbnailPath" AS "asset_thumbnailPath",
   "asset"."thumbhash" AS "asset_thumbhash",
   "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
   "asset"."createdAt" AS "asset_createdAt",
diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql
index 2880e6896f..10af8d17db 100644
--- a/server/src/queries/shared.link.repository.sql
+++ b/server/src/queries/shared.link.repository.sql
@@ -28,8 +28,6 @@ FROM
       "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
       "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
       "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
-      "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath",
-      "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath",
       "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
       "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
       "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
@@ -96,8 +94,6 @@ FROM
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
-      "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath",
-      "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
       "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt",
@@ -218,8 +214,6 @@ SELECT
   "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
   "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
   "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
-  "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath",
-  "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath",
   "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
   "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
   "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index 80b26a67bf..a74451f9a5 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -1,11 +1,12 @@
 import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
 import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-import { AssetOrder, AssetType } from 'src/enum';
+import { AssetFileType, AssetOrder, AssetType } from 'src/enum';
 import {
   AssetBuilderOptions,
   AssetCreate,
@@ -59,6 +60,7 @@ const dateTrunc = (options: TimeBucketOptions) =>
 export class AssetRepository implements IAssetRepository {
   constructor(
     @InjectRepository(AssetEntity) private repository: Repository<AssetEntity>,
+    @InjectRepository(AssetFileEntity) private fileRepository: Repository<AssetFileEntity>,
     @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
     @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
     @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository<SmartInfoEntity>,
@@ -84,7 +86,6 @@ export class AssetRepository implements IAssetRepository {
         `entity.ownerId IN (:...ownerIds)
       AND entity.isVisible = true
       AND entity.isArchived = false
-      AND entity.previewPath IS NOT NULL
       AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day
       AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`,
         {
@@ -94,6 +95,7 @@ export class AssetRepository implements IAssetRepository {
         },
       )
       .leftJoinAndSelect('entity.exifInfo', 'exifInfo')
+      .leftJoinAndSelect('entity.files', 'files')
       .orderBy('entity.localDateTime', 'ASC')
       .getMany();
   }
@@ -128,6 +130,7 @@ export class AssetRepository implements IAssetRepository {
         stack: {
           assets: true,
         },
+        files: true,
       },
       withDeleted: true,
     });
@@ -214,7 +217,7 @@ export class AssetRepository implements IAssetRepository {
   }
 
   getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
-    let builder = this.repository.createQueryBuilder('asset');
+    let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files');
     builder = searchAssetBuilder(builder, options);
     builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC');
     return paginatedBuilder<AssetEntity>(builder, {
@@ -706,7 +709,11 @@ export class AssetRepository implements IAssetRepository {
   }
 
   private getBuilder(options: AssetBuilderOptions) {
-    const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true');
+    const builder = this.repository
+      .createQueryBuilder('asset')
+      .where('asset.isVisible = true')
+      .leftJoinAndSelect('asset.files', 'files');
+
     if (options.assetType !== undefined) {
       builder.andWhere('asset.type = :assetType', { assetType: options.assetType });
     }
@@ -812,4 +819,9 @@ export class AssetRepository implements IAssetRepository {
       .withDeleted();
     return builder.getMany();
   }
+
+  @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
+  async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise<void> {
+    await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] });
+  }
 }
diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts
index 978f98cf10..2f5192d84f 100644
--- a/server/src/services/asset-media.service.spec.ts
+++ b/server/src/services/asset-media.service.spec.ts
@@ -2,6 +2,7 @@ import { BadRequestException, NotFoundException, UnauthorizedException } from '@
 import { Stats } from 'node:fs';
 import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
 import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
 import { AssetType } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
@@ -150,15 +151,14 @@ const assetEntity = Object.freeze({
   deviceId: 'device_id_1',
   type: AssetType.VIDEO,
   originalPath: 'fake_path/asset_1.jpeg',
-  previewPath: '',
   fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
   fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
   updatedAt: new Date('2022-06-19T23:41:36.910Z'),
   isFavorite: false,
   isArchived: false,
-  thumbnailPath: '',
   encodedVideoPath: '',
   duration: '0:00:00.000000',
+  files: [] as AssetFileEntity[],
   exifInfo: {
     latitude: 49.533_547,
     longitude: 10.703_075,
@@ -418,7 +418,7 @@ describe(AssetMediaService.name, () => {
 
       await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException);
 
-      expect(assetMock.getById).toHaveBeenCalledWith('asset-1');
+      expect(assetMock.getById).toHaveBeenCalledWith('asset-1', { files: true });
     });
 
     it('should download a file', async () => {
diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts
index b8a43b34ec..b66b0607b3 100644
--- a/server/src/services/asset-media.service.ts
+++ b/server/src/services/asset-media.service.ts
@@ -36,6 +36,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 import { mimeTypes } from 'src/utils/mime-types';
 import { fromChecksum } from 'src/utils/request';
@@ -238,9 +239,10 @@ export class AssetMediaService {
     const asset = await this.findOrFail(id);
     const size = dto.size ?? AssetMediaSize.THUMBNAIL;
 
-    let filepath = asset.previewPath;
-    if (size === AssetMediaSize.THUMBNAIL && asset.thumbnailPath) {
-      filepath = asset.thumbnailPath;
+    const { thumbnailFile, previewFile } = getAssetFiles(asset.files);
+    let filepath = previewFile?.path;
+    if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) {
+      filepath = thumbnailFile.path;
     }
 
     if (!filepath) {
@@ -460,7 +462,7 @@ export class AssetMediaService {
   }
 
   private async findOrFail(id: string): Promise<AssetEntity> {
-    const asset = await this.assetRepository.getById(id);
+    const asset = await this.assetRepository.getById(id, { files: true });
     if (!asset) {
       throw new NotFoundException('Asset not found');
     }
diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts
index f79b2819ff..3ac7aa1c71 100755
--- a/server/src/services/asset.service.spec.ts
+++ b/server/src/services/asset.service.spec.ts
@@ -299,8 +299,8 @@ describe(AssetService.name, () => {
             name: JobName.DELETE_FILES,
             data: {
               files: [
-                assetWithFace.thumbnailPath,
-                assetWithFace.previewPath,
+                '/uploads/user-id/webp/path.ext',
+                '/uploads/user-id/thumbs/path.jpg',
                 assetWithFace.encodedVideoPath,
                 assetWithFace.sidecarPath,
                 assetWithFace.originalPath,
diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts
index 94a3ba1603..e9aefce910 100644
--- a/server/src/services/asset.service.ts
+++ b/server/src/services/asset.service.ts
@@ -39,7 +39,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
 import { IStackRepository } from 'src/interfaces/stack.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
-import { getMyPartnerIds } from 'src/utils/asset.util';
+import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util';
 import { usePagination } from 'src/utils/pagination';
 
 export class AssetService {
@@ -71,9 +71,10 @@ export class AssetService {
     const userIds = [auth.user.id, ...partnerIds];
 
     const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
+    const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile);
     const groups: Record<number, AssetEntity[]> = {};
     const currentYear = new Date().getFullYear();
-    for (const asset of assets) {
+    for (const asset of assetsWithThumbnails) {
       const yearsAgo = currentYear - asset.localDateTime.getFullYear();
       if (!groups[yearsAgo]) {
         groups[yearsAgo] = [];
@@ -126,6 +127,7 @@ export class AssetService {
             exifInfo: true,
           },
         },
+        files: true,
       },
       {
         faces: {
@@ -170,6 +172,7 @@ export class AssetService {
       faces: {
         person: true,
       },
+      files: true,
     });
     if (!asset) {
       throw new BadRequestException('Asset not found');
@@ -223,6 +226,7 @@ export class AssetService {
       library: true,
       stack: { assets: true },
       exifInfo: true,
+      files: true,
     });
 
     if (!asset) {
@@ -260,7 +264,8 @@ export class AssetService {
       }
     }
 
-    const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
+    const { thumbnailFile, previewFile } = getAssetFiles(asset.files);
+    const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath];
     if (deleteOnDisk) {
       files.push(asset.sidecarPath, asset.originalPath);
     }
diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts
index 225bd11061..734ed9b7c3 100644
--- a/server/src/services/audit.service.ts
+++ b/server/src/services/audit.service.ts
@@ -14,7 +14,7 @@ import {
 } from 'src/dtos/audit.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
 import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { DatabaseAction, Permission } from 'src/enum';
+import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
 import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IAuditRepository } from 'src/interfaces/audit.interface';
@@ -24,6 +24,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { usePagination } from 'src/utils/pagination';
 
 @Injectable()
@@ -97,12 +98,12 @@ export class AuditService {
         }
 
         case AssetPathType.PREVIEW: {
-          await this.assetRepository.update({ id, previewPath: pathValue });
+          await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue });
           break;
         }
 
         case AssetPathType.THUMBNAIL: {
-          await this.assetRepository.update({ id, thumbnailPath: pathValue });
+          await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue });
           break;
         }
 
@@ -155,7 +156,7 @@ export class AuditService {
       }
     }
 
-    const track = (filename: string | null) => {
+    const track = (filename: string | null | undefined) => {
       if (!filename) {
         return;
       }
@@ -175,8 +176,9 @@ export class AuditService {
     const orphans: FileReportItemDto[] = [];
     for await (const assets of pagination) {
       assetCount += assets.length;
-      for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) {
-        for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) {
+      for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) {
+        const { previewFile, thumbnailFile } = getAssetFiles(files);
+        for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) {
           track(file);
         }
 
@@ -192,11 +194,11 @@ export class AuditService {
         ) {
           orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath });
         }
-        if (previewPath && !hasFile(thumbFiles, previewPath)) {
-          orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath });
+        if (previewFile && !hasFile(thumbFiles, previewFile.path)) {
+          orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path });
         }
-        if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
-          orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath });
+        if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) {
+          orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path });
         }
         if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
           orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath });
diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts
index 70852a5381..35a1a7325b 100644
--- a/server/src/services/duplicate.service.ts
+++ b/server/src/services/duplicate.service.ts
@@ -17,6 +17,7 @@ import {
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { isDuplicateDetectionEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 
@@ -69,7 +70,7 @@ export class DuplicateService {
       return JobStatus.SKIPPED;
     }
 
-    const asset = await this.assetRepository.getById(id, { smartSearch: true });
+    const asset = await this.assetRepository.getById(id, { files: true, smartSearch: true });
     if (!asset) {
       this.logger.error(`Asset ${id} not found`);
       return JobStatus.FAILED;
@@ -80,7 +81,8 @@ export class DuplicateService {
       return JobStatus.SKIPPED;
     }
 
-    if (!asset.previewPath) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (!previewFile) {
       this.logger.warn(`Asset ${id} is missing preview image`);
       return JobStatus.FAILED;
     }
diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts
index d9d5948cea..634cd790eb 100644
--- a/server/src/services/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -9,7 +9,7 @@ import {
   VideoCodec,
 } from 'src/config';
 import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetType } from 'src/enum';
+import { AssetFileType, AssetType } from 'src/enum';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -298,18 +298,20 @@ describe(MediaService.name, () => {
         colorspace: Colorspace.SRGB,
         processInvalidImages: false,
       });
-      expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'asset-id',
+        type: AssetFileType.PREVIEW,
+        path: previewPath,
+      });
     });
 
     it('should delete previous preview if different path', async () => {
-      const previousPreviewPath = assetStub.image.previewPath;
-
       systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
 
       await sut.handleGeneratePreview({ id: assetStub.image.id });
 
-      expect(storageMock.unlink).toHaveBeenCalledWith(previousPreviewPath);
+      expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
     });
 
     it('should generate a P3 thumbnail for a wide gamut image', async () => {
@@ -330,9 +332,10 @@ describe(MediaService.name, () => {
           processInvalidImages: false,
         },
       );
-      expect(assetMock.update).toHaveBeenCalledWith({
-        id: 'asset-id',
-        previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'asset-id',
+        type: AssetFileType.PREVIEW,
+        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
       });
     });
 
@@ -357,9 +360,10 @@ describe(MediaService.name, () => {
           twoPass: false,
         },
       );
-      expect(assetMock.update).toHaveBeenCalledWith({
-        id: 'asset-id',
-        previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'asset-id',
+        type: AssetFileType.PREVIEW,
+        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
       });
     });
 
@@ -384,9 +388,10 @@ describe(MediaService.name, () => {
           twoPass: false,
         },
       );
-      expect(assetMock.update).toHaveBeenCalledWith({
-        id: 'asset-id',
-        previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
+      expect(assetMock.upsertFile).toHaveBeenCalledWith({
+        assetId: 'asset-id',
+        type: AssetFileType.PREVIEW,
+        path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
       });
     });
 
@@ -472,19 +477,21 @@ describe(MediaService.name, () => {
           colorspace: Colorspace.SRGB,
           processInvalidImages: false,
         });
-        expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
+        expect(assetMock.upsertFile).toHaveBeenCalledWith({
+          assetId: 'asset-id',
+          type: AssetFileType.THUMBNAIL,
+          path: thumbnailPath,
+        });
       },
     );
 
     it('should delete previous thumbnail if different path', async () => {
-      const previousThumbnailPath = assetStub.image.thumbnailPath;
-
       systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } });
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
 
       await sut.handleGenerateThumbnail({ id: assetStub.image.id });
 
-      expect(storageMock.unlink).toHaveBeenCalledWith(previousThumbnailPath);
+      expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext');
     });
   });
 
@@ -504,9 +511,10 @@ describe(MediaService.name, () => {
         processInvalidImages: false,
       },
     );
-    expect(assetMock.update).toHaveBeenCalledWith({
-      id: 'asset-id',
-      thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
+    expect(assetMock.upsertFile).toHaveBeenCalledWith({
+      assetId: 'asset-id',
+      type: AssetFileType.THUMBNAIL,
+      path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
     });
   });
 
diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts
index ff77cbb34e..b48d00a7a8 100644
--- a/server/src/services/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -15,7 +15,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
 import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetPathType } from 'src/entities/move.entity';
-import { AssetType } from 'src/enum';
+import { AssetFileType, AssetType } from 'src/enum';
 import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
 import { ICryptoRepository } from 'src/interfaces/crypto.interface';
 import {
@@ -34,6 +34,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
 import { mimeTypes } from 'src/utils/mime-types';
 import { usePagination } from 'src/utils/pagination';
@@ -72,7 +73,11 @@ export class MediaService {
   async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
     const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
       return force
-        ? this.assetRepository.getAll(pagination, { isVisible: true, withDeleted: true, withArchived: true })
+        ? this.assetRepository.getAll(pagination, {
+            isVisible: true,
+            withDeleted: true,
+            withArchived: true,
+          })
         : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
     });
 
@@ -80,13 +85,17 @@ export class MediaService {
       const jobs: JobItem[] = [];
 
       for (const asset of assets) {
-        if (!asset.previewPath || force) {
+        const { previewFile, thumbnailFile } = getAssetFiles(asset.files);
+
+        if (!previewFile || force) {
           jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
           continue;
         }
-        if (!asset.thumbnailPath) {
+
+        if (!thumbnailFile) {
           jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
         }
+
         if (!asset.thumbhash) {
           jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } });
         }
@@ -152,7 +161,7 @@ export class MediaService {
 
   async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
     const { image } = await this.configCore.getConfig({ withCache: true });
-    const [asset] = await this.assetRepository.getByIds([id]);
+    const [asset] = await this.assetRepository.getByIds([id], { files: true });
     if (!asset) {
       return JobStatus.FAILED;
     }
@@ -182,12 +191,14 @@ export class MediaService {
       return JobStatus.SKIPPED;
     }
 
-    if (asset.previewPath && asset.previewPath !== previewPath) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (previewFile && previewFile.path !== previewPath) {
       this.logger.debug(`Deleting old preview for asset ${asset.id}`);
-      await this.storageRepository.unlink(asset.previewPath);
+      await this.storageRepository.unlink(previewFile.path);
     }
 
-    await this.assetRepository.update({ id: asset.id, previewPath });
+    await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath });
+    await this.assetRepository.update({ id: asset.id, updatedAt: new Date() });
     await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() });
 
     return JobStatus.SUCCESS;
@@ -253,7 +264,7 @@ export class MediaService {
   async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
     const [{ image }, [asset]] = await Promise.all([
       this.configCore.getConfig({ withCache: true }),
-      this.assetRepository.getByIds([id], { exifInfo: true }),
+      this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
     ]);
     if (!asset) {
       return JobStatus.FAILED;
@@ -268,19 +279,21 @@ export class MediaService {
       return JobStatus.SKIPPED;
     }
 
-    if (asset.thumbnailPath && asset.thumbnailPath !== thumbnailPath) {
+    const { thumbnailFile } = getAssetFiles(asset.files);
+    if (thumbnailFile && thumbnailFile.path !== thumbnailPath) {
       this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`);
-      await this.storageRepository.unlink(asset.thumbnailPath);
+      await this.storageRepository.unlink(thumbnailFile.path);
     }
 
-    await this.assetRepository.update({ id: asset.id, thumbnailPath });
+    await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath });
+    await this.assetRepository.update({ id: asset.id, updatedAt: new Date() });
     await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() });
 
     return JobStatus.SUCCESS;
   }
 
   async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
-    const [asset] = await this.assetRepository.getByIds([id]);
+    const [asset] = await this.assetRepository.getByIds([id], { files: true });
     if (!asset) {
       return JobStatus.FAILED;
     }
@@ -289,11 +302,12 @@ export class MediaService {
       return JobStatus.SKIPPED;
     }
 
-    if (!asset.previewPath) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (!previewFile) {
       return JobStatus.FAILED;
     }
 
-    const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath);
+    const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path);
     await this.assetRepository.update({ id: asset.id, thumbhash });
 
     return JobStatus.SUCCESS;
diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts
index 74d2a12127..bcce902e91 100644
--- a/server/src/services/notification.service.spec.ts
+++ b/server/src/services/notification.service.spec.ts
@@ -1,6 +1,7 @@
 import { defaults, SystemConfig } from 'src/config';
 import { AlbumUserEntity } from 'src/entities/album-user.entity';
-import { UserMetadataKey } from 'src/enum';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
+import { AssetFileType, UserMetadataKey } from 'src/enum';
 import { IAlbumRepository } from 'src/interfaces/album.interface';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -333,7 +334,9 @@ describe(NotificationService.name, () => {
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
 
       await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
-      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId);
+      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
+        files: true,
+      });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.SEND_EMAIL,
         data: expect.objectContaining({
@@ -358,10 +361,15 @@ describe(NotificationService.name, () => {
       });
       systemMock.get.mockResolvedValue({ server: {} });
       notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' });
-      assetMock.getById.mockResolvedValue({ ...assetStub.image, thumbnailPath: 'path-to-thumb.jpg' });
+      assetMock.getById.mockResolvedValue({
+        ...assetStub.image,
+        files: [{ assetId: 'asset-id', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' } as AssetFileEntity],
+      });
 
       await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
-      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId);
+      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
+        files: true,
+      });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.SEND_EMAIL,
         data: expect.objectContaining({
@@ -389,7 +397,9 @@ describe(NotificationService.name, () => {
       assetMock.getById.mockResolvedValue(assetStub.image);
 
       await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS);
-      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId);
+      expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, {
+        files: true,
+      });
       expect(jobMock.queue).toHaveBeenCalledWith({
         name: JobName.SEND_EMAIL,
         data: expect.objectContaining({
diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts
index 80abc4ca98..31701013b7 100644
--- a/server/src/services/notification.service.ts
+++ b/server/src/services/notification.service.ts
@@ -21,6 +21,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 import { IUserRepository } from 'src/interfaces/user.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { getFilenameExtension } from 'src/utils/file';
 import { getPreferences } from 'src/utils/preferences';
 
@@ -268,14 +269,15 @@ export class NotificationService {
       return;
     }
 
-    const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId);
-    if (!albumThumbnail?.thumbnailPath) {
+    const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true });
+    const { thumbnailFile } = getAssetFiles(albumThumbnail?.files);
+    if (!thumbnailFile) {
       return;
     }
 
     return {
-      filename: `album-thumbnail${getFilenameExtension(albumThumbnail.thumbnailPath)}`,
-      path: albumThumbnail.thumbnailPath,
+      filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`,
+      path: thumbnailFile.path,
       cid: 'album-thumbnail',
     };
   }
diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts
index 70e043cc7f..f8608243ae 100644
--- a/server/src/services/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -716,7 +716,7 @@ describe(PersonService.name, () => {
       await sut.handleDetectFaces({ id: assetStub.image.id });
       expect(machineLearningMock.detectFaces).toHaveBeenCalledWith(
         'http://immich-machine-learning:3003',
-        assetStub.image.previewPath,
+        '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
       );
       expect(personMock.createFaces).not.toHaveBeenCalled();
@@ -946,7 +946,7 @@ describe(PersonService.name, () => {
 
       await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
 
-      expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true });
+      expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true, files: true });
       expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs');
       expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
         assetStub.primaryImage.originalPath,
@@ -1032,7 +1032,7 @@ describe(PersonService.name, () => {
       await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
 
       expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
-        assetStub.video.previewPath,
+        '/uploads/user-id/thumbs/path.jpg',
         'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
         {
           format: 'jpeg',
diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts
index 6d536f4bf8..3fc34d8b15 100644
--- a/server/src/services/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -50,6 +50,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf
 import { ISearchRepository } from 'src/interfaces/search.interface';
 import { IStorageRepository } from 'src/interfaces/storage.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 import { mimeTypes } from 'src/utils/mime-types';
 import { isFacialRecognitionEnabled } from 'src/utils/misc';
@@ -333,9 +334,11 @@ export class PersonService {
       faces: {
         person: false,
       },
+      files: true,
     };
     const [asset] = await this.assetRepository.getByIds([id], relations);
-    if (!asset || !asset.previewPath || asset.faces?.length > 0) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (!asset || !previewFile || asset.faces?.length > 0) {
       return JobStatus.FAILED;
     }
 
@@ -349,11 +352,11 @@ export class PersonService {
 
     const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces(
       machineLearning.url,
-      asset.previewPath,
+      previewFile.path,
       machineLearning.facialRecognition,
     );
 
-    this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`);
+    this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`);
 
     if (faces.length > 0) {
       await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
@@ -549,7 +552,10 @@ export class PersonService {
       imageHeight: oldHeight,
     } = face;
 
-    const asset = await this.assetRepository.getById(assetId, { exifInfo: true });
+    const asset = await this.assetRepository.getById(assetId, {
+      exifInfo: true,
+      files: true,
+    });
     if (!asset) {
       this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`);
       return JobStatus.FAILED;
@@ -646,7 +652,8 @@ export class PersonService {
       throw new Error(`Asset ${asset.id} dimensions are unknown`);
     }
 
-    if (!asset.previewPath) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (!previewFile) {
       throw new Error(`Asset ${asset.id} has no preview path`);
     }
 
@@ -659,8 +666,8 @@ export class PersonService {
       return { width, height, inputPath: asset.originalPath };
     }
 
-    const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath);
-    return { width, height, inputPath: asset.previewPath };
+    const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path);
+    return { width, height, inputPath: previewFile.path };
   }
 
   private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions {
diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts
index 278e06d287..97d22da9b8 100644
--- a/server/src/services/smart-info.service.spec.ts
+++ b/server/src/services/smart-info.service.spec.ts
@@ -318,7 +318,7 @@ describe(SmartInfoService.name, () => {
 
       expect(machineMock.encodeImage).toHaveBeenCalledWith(
         'http://immich-machine-learning:3003',
-        assetStub.image.previewPath,
+        '/uploads/user-id/thumbs/path.jpg',
         expect.objectContaining({ modelName: 'ViT-B-32__openai' }),
       );
       expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts
index 883f320abf..d57b5fb54f 100644
--- a/server/src/services/smart-info.service.ts
+++ b/server/src/services/smart-info.service.ts
@@ -18,6 +18,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
 import { ISearchRepository } from 'src/interfaces/search.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { getAssetFiles } from 'src/utils/asset.util';
 import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
 import { usePagination } from 'src/utils/pagination';
 
@@ -135,7 +136,7 @@ export class SmartInfoService {
       return JobStatus.SKIPPED;
     }
 
-    const [asset] = await this.assetRepository.getByIds([id]);
+    const [asset] = await this.assetRepository.getByIds([id], { files: true });
     if (!asset) {
       return JobStatus.FAILED;
     }
@@ -144,13 +145,14 @@ export class SmartInfoService {
       return JobStatus.SKIPPED;
     }
 
-    if (!asset.previewPath) {
+    const { previewFile } = getAssetFiles(asset.files);
+    if (!previewFile) {
       return JobStatus.FAILED;
     }
 
     const embedding = await this.machineLearning.encodeImage(
       machineLearning.url,
-      asset.previewPath,
+      previewFile.path,
       machineLearning.clip,
     );
 
diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts
index aa77a0b144..31f708611d 100644
--- a/server/src/utils/asset.util.ts
+++ b/server/src/utils/asset.util.ts
@@ -1,7 +1,8 @@
 import { AccessCore } from 'src/cores/access.core';
 import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
 import { AuthDto } from 'src/dtos/auth.dto';
-import { Permission } from 'src/enum';
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
+import { AssetFileType, Permission } from 'src/enum';
 import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IPartnerRepository } from 'src/interfaces/partner.interface';
 
@@ -11,6 +12,15 @@ export interface IBulkAsset {
   removeAssetIds: (id: string, assetIds: string[]) => Promise<void>;
 }
 
+const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => {
+  return (files || []).find((file) => file.type === type);
+};
+
+export const getAssetFiles = (files?: AssetFileEntity[]) => ({
+  previewFile: getFileByType(files, AssetFileType.PREVIEW),
+  thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL),
+});
+
 export const addAssets = async (
   auth: AuthDto,
   repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts
index 21a40ffcc8..f3232eb78b 100644
--- a/server/src/utils/database.ts
+++ b/server/src/utils/database.ts
@@ -71,7 +71,7 @@ export function searchAssetBuilder(
     builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds });
   }
 
-  const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']);
+  const path = _.pick(options, ['encodedVideoPath', 'originalPath']);
   builder.andWhere(_.omitBy(path, _.isUndefined));
 
   if (options.originalFileName) {
diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts
index 23df5e4f56..b8c7e06d82 100644
--- a/server/test/fixtures/asset.stub.ts
+++ b/server/test/fixtures/asset.stub.ts
@@ -1,12 +1,33 @@
+import { AssetFileEntity } from 'src/entities/asset-files.entity';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { ExifEntity } from 'src/entities/exif.entity';
 import { StackEntity } from 'src/entities/stack.entity';
-import { AssetType } from 'src/enum';
+import { AssetFileType, AssetType } from 'src/enum';
 import { authStub } from 'test/fixtures/auth.stub';
 import { fileStub } from 'test/fixtures/file.stub';
 import { libraryStub } from 'test/fixtures/library.stub';
 import { userStub } from 'test/fixtures/user.stub';
 
+const previewFile: AssetFileEntity = {
+  id: 'file-1',
+  assetId: 'asset-id',
+  type: AssetFileType.PREVIEW,
+  path: '/uploads/user-id/thumbs/path.jpg',
+  createdAt: new Date('2023-02-23T05:06:29.716Z'),
+  updatedAt: new Date('2023-02-23T05:06:29.716Z'),
+};
+
+const thumbnailFile: AssetFileEntity = {
+  id: 'file-2',
+  assetId: 'asset-id',
+  type: AssetFileType.THUMBNAIL,
+  path: '/uploads/user-id/webp/path.ext',
+  createdAt: new Date('2023-02-23T05:06:29.716Z'),
+  updatedAt: new Date('2023-02-23T05:06:29.716Z'),
+};
+
+const files: AssetFileEntity[] = [previewFile, thumbnailFile];
+
 export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => {
   return {
     id: stackId,
@@ -29,10 +50,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: 'upload/library/IMG_123.jpg',
-    previewPath: null,
+    files: [thumbnailFile],
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -63,10 +83,10 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: 'upload/library/IMG_456.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
+
+    files: [previewFile],
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: null,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -101,10 +121,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
+    files,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
     thumbhash: null,
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -136,10 +155,9 @@ export const assetStub = {
     ownerId: 'admin-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/admin-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
+    files,
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/admin-id/webp/path.ext',
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -181,10 +199,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
+    files,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -221,10 +238,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -261,10 +277,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -301,10 +316,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/data/user1/photo.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('path hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -341,10 +355,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -379,10 +392,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/data/user1/photo.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('path hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -419,10 +431,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -457,10 +468,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
+    files,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2015-02-23T05:06:29.716Z'),
@@ -496,10 +506,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.VIDEO,
-    thumbnailPath: null,
+    files: [previewFile],
     thumbhash: null,
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -548,8 +557,22 @@ export const assetStub = {
     isVisible: false,
     fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
     fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
-    previewPath: '/uploads/user-id/thumbs/path.ext',
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files: [
+      {
+        assetId: 'asset-id',
+        type: AssetFileType.PREVIEW,
+        path: '/uploads/user-id/thumbs/path.ext',
+        createdAt: new Date('2023-02-23T05:06:29.716Z'),
+        updatedAt: new Date('2023-02-23T05:06:29.716Z'),
+      },
+      {
+        assetId: 'asset-id',
+        type: AssetFileType.THUMBNAIL,
+        path: '/uploads/user-id/webp/path.ext',
+        createdAt: new Date('2023-02-23T05:06:29.716Z'),
+        updatedAt: new Date('2023-02-23T05:06:29.716Z'),
+      },
+    ],
     exifInfo: {
       fileSizeInByte: 100_000,
       timeZone: `America/New_York`,
@@ -612,10 +635,9 @@ export const assetStub = {
     deviceId: 'device-id',
     checksum: Buffer.from('file hash', 'utf8'),
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     sidecarPath: null,
     type: AssetType.IMAGE,
-    thumbnailPath: null,
+    files: [previewFile],
     thumbhash: null,
     encodedVideoPath: null,
     createdAt: new Date('2023-02-22T05:06:29.716Z'),
@@ -653,11 +675,10 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     thumbhash: null,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: null,
+    files: [previewFile],
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -687,11 +708,10 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     thumbhash: null,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: null,
+    files: [previewFile],
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -722,11 +742,10 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     thumbhash: null,
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: null,
+    files: [previewFile],
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
     updatedAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -758,10 +777,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.ext',
-    previewPath: '/uploads/user-id/thumbs/path.ext',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.VIDEO,
-    thumbnailPath: null,
+    files: [previewFile],
     thumbhash: null,
     encodedVideoPath: '/encoded/video/path.mp4',
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -794,10 +812,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/data/user1/photo.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -833,10 +850,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/data/user1/photo.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -872,10 +888,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.dng',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -911,10 +926,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
@@ -952,10 +966,9 @@ export const assetStub = {
     ownerId: 'user-id',
     deviceId: 'device-id',
     originalPath: '/original/path.jpg',
-    previewPath: '/uploads/user-id/thumbs/path.jpg',
     checksum: Buffer.from('file hash', 'utf8'),
     type: AssetType.IMAGE,
-    thumbnailPath: '/uploads/user-id/webp/path.ext',
+    files,
     thumbhash: Buffer.from('blablabla', 'base64'),
     encodedVideoPath: null,
     createdAt: new Date('2023-02-23T05:06:29.716Z'),
diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts
index 8a5cc17d4f..9ea252b5f7 100644
--- a/server/test/fixtures/shared-link.stub.ts
+++ b/server/test/fixtures/shared-link.stub.ts
@@ -196,7 +196,6 @@ export const sharedLinkStub = {
           deviceId: 'device_id_1',
           type: AssetType.VIDEO,
           originalPath: 'fake_path/jpeg',
-          previewPath: '',
           checksum: Buffer.from('file hash', 'utf8'),
           fileModifiedAt: today,
           fileCreatedAt: today,
@@ -213,7 +212,7 @@ export const sharedLinkStub = {
             objects: ['a', 'b', 'c'],
             asset: null as any,
           },
-          thumbnailPath: '',
+          files: [],
           thumbhash: null,
           encodedVideoPath: '',
           duration: null,
diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts
index f1091c041f..9320639b93 100644
--- a/server/test/repositories/asset.repository.mock.ts
+++ b/server/test/repositories/asset.repository.mock.ts
@@ -42,5 +42,6 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
     getAllForUserFullSync: vitest.fn(),
     getChangedDeltaSync: vitest.fn(),
     getDuplicates: vitest.fn(),
+    upsertFile: vitest.fn(),
   };
 };