diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b6b17774ed..9adcfe7373 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,23 +10,6 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  server-e2e-api:
-    name: Server (e2e-api)
-    runs-on: ubuntu-latest
-    defaults:
-      run:
-        working-directory: ./server
-
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Run npm install
-        run: npm ci
-
-      - name: Run e2e tests
-        run: npm run e2e:api
-
   server-e2e-jobs:
     name: Server (e2e-jobs)
     runs-on: ubuntu-latest
diff --git a/Makefile b/Makefile
index b455e2656b..55875e732b 100644
--- a/Makefile
+++ b/Makefile
@@ -19,9 +19,6 @@ pull-stage:
 server-e2e-jobs:
 	docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
 
-server-e2e-api:
-	npm run e2e:api --prefix server
-
 .PHONY: e2e
 e2e:
 	docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
index f1bb355315..65ad094be6 100644
--- a/e2e/src/api/specs/asset.e2e-spec.ts
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -2,20 +2,45 @@ import {
   AssetFileUploadResponseDto,
   AssetResponseDto,
   AssetTypeEnum,
+  LibraryResponseDto,
   LoginResponseDto,
   SharedLinkType,
+  TimeBucketSize,
+  getAllLibraries,
+  getAssetInfo,
+  updateAssets,
 } from '@immich/sdk';
 import { exiftool } from 'exiftool-vendored';
 import { DateTime } from 'luxon';
+import { randomBytes } from 'node:crypto';
 import { readFile, writeFile } from 'node:fs/promises';
 import { basename, join } from 'node:path';
 import { Socket } from 'socket.io-client';
 import { createUserDto, uuidDto } from 'src/fixtures';
+import { makeRandomImage } from 'src/generators';
 import { errorDto } from 'src/responses';
-import { app, tempDir, testAssetDir, utils } from 'src/utils';
+import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
 import request from 'supertest';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
+const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
+  const dto: Record<string, any> = {
+    deviceAssetId: 'example-image',
+    deviceId: 'TEST',
+    fileCreatedAt: new Date().toISOString(),
+    fileModifiedAt: new Date().toISOString(),
+    isFavorite: 'testing',
+    duration: '0:00:00.000000',
+  };
+
+  const omit = options?.omit;
+  if (omit) {
+    delete dto[omit];
+  }
+
+  return dto;
+};
+
 const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
 
 const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
@@ -35,34 +60,43 @@ const yesterday = today.minus({ days: 1 });
 
 describe('/asset', () => {
   let admin: LoginResponseDto;
+  let websocket: Socket;
+
   let user1: LoginResponseDto;
   let user2: LoginResponseDto;
-  let userStats: LoginResponseDto;
+  let timeBucketUser: LoginResponseDto;
+  let quotaUser: LoginResponseDto;
+  let statsUser: LoginResponseDto;
+  let stackUser: LoginResponseDto;
+
   let user1Assets: AssetFileUploadResponseDto[];
   let user2Assets: AssetFileUploadResponseDto[];
-  let assetLocation: AssetFileUploadResponseDto;
-  let ws: Socket;
+  let stackAssets: AssetFileUploadResponseDto[];
+  let locationAsset: AssetFileUploadResponseDto;
 
   beforeAll(async () => {
     await utils.resetDatabase();
     admin = await utils.adminSetup({ onboarding: false });
 
-    [ws, user1, user2, userStats] = await Promise.all([
+    [websocket, user1, user2, statsUser, quotaUser, timeBucketUser, stackUser] = await Promise.all([
       utils.connectWebsocket(admin.accessToken),
-      utils.userSetup(admin.accessToken, createUserDto.user1),
-      utils.userSetup(admin.accessToken, createUserDto.user2),
-      utils.userSetup(admin.accessToken, createUserDto.user3),
+      utils.userSetup(admin.accessToken, createUserDto.create('1')),
+      utils.userSetup(admin.accessToken, createUserDto.create('2')),
+      utils.userSetup(admin.accessToken, createUserDto.create('stats')),
+      utils.userSetup(admin.accessToken, createUserDto.userQuota),
+      utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
+      utils.userSetup(admin.accessToken, createUserDto.create('stack')),
     ]);
 
     // asset location
-    assetLocation = await utils.createAsset(admin.accessToken, {
+    locationAsset = await utils.createAsset(admin.accessToken, {
       assetData: {
         filename: 'thompson-springs.jpg',
         bytes: await readFile(locationAssetFilepath),
       },
     });
 
-    await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id });
+    await utils.waitForWebsocketEvent({ event: 'upload', assetId: locationAsset.id });
 
     user1Assets = await Promise.all([
       utils.createAsset(user1.accessToken),
@@ -80,22 +114,43 @@ describe('/asset', () => {
 
     user2Assets = await Promise.all([utils.createAsset(user2.accessToken)]);
 
+    await Promise.all([
+      utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
+      utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
+      utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
+      utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
+    ]);
+
     for (const asset of [...user1Assets, ...user2Assets]) {
       expect(asset.duplicate).toBe(false);
     }
 
     await Promise.all([
       // stats
-      utils.createAsset(userStats.accessToken),
-      utils.createAsset(userStats.accessToken, { isFavorite: true }),
-      utils.createAsset(userStats.accessToken, { isArchived: true }),
-      utils.createAsset(userStats.accessToken, {
+      utils.createAsset(statsUser.accessToken),
+      utils.createAsset(statsUser.accessToken, { isFavorite: true }),
+      utils.createAsset(statsUser.accessToken, { isArchived: true }),
+      utils.createAsset(statsUser.accessToken, {
         isArchived: true,
         isFavorite: true,
         assetData: { filename: 'example.mp4' },
       }),
     ]);
 
+    // stacks
+    stackAssets = await Promise.all([
+      utils.createAsset(stackUser.accessToken),
+      utils.createAsset(stackUser.accessToken),
+      utils.createAsset(stackUser.accessToken),
+      utils.createAsset(stackUser.accessToken),
+      utils.createAsset(stackUser.accessToken),
+    ]);
+
+    await updateAssets(
+      { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
+      { headers: asBearerAuth(stackUser.accessToken) },
+    );
+
     const person1 = await utils.createPerson(user1.accessToken, {
       name: 'Test Person',
     });
@@ -106,7 +161,7 @@ describe('/asset', () => {
   }, 30_000);
 
   afterAll(() => {
-    utils.disconnectWebsocket(ws);
+    utils.disconnectWebsocket(websocket);
   });
 
   describe('GET /asset/:id', () => {
@@ -193,7 +248,7 @@ describe('/asset', () => {
     it('should return stats of all assets', async () => {
       const { status, body } = await request(app)
         .get('/asset/statistics')
-        .set('Authorization', `Bearer ${userStats.accessToken}`);
+        .set('Authorization', `Bearer ${statsUser.accessToken}`);
 
       expect(body).toEqual({ images: 3, videos: 1, total: 4 });
       expect(status).toBe(200);
@@ -202,7 +257,7 @@ describe('/asset', () => {
     it('should return stats of all favored assets', async () => {
       const { status, body } = await request(app)
         .get('/asset/statistics')
-        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .set('Authorization', `Bearer ${statsUser.accessToken}`)
         .query({ isFavorite: true });
 
       expect(status).toBe(200);
@@ -212,7 +267,7 @@ describe('/asset', () => {
     it('should return stats of all archived assets', async () => {
       const { status, body } = await request(app)
         .get('/asset/statistics')
-        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .set('Authorization', `Bearer ${statsUser.accessToken}`)
         .query({ isArchived: true });
 
       expect(status).toBe(200);
@@ -222,7 +277,7 @@ describe('/asset', () => {
     it('should return stats of all favored and archived assets', async () => {
       const { status, body } = await request(app)
         .get('/asset/statistics')
-        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .set('Authorization', `Bearer ${statsUser.accessToken}`)
         .query({ isFavorite: true, isArchived: true });
 
       expect(status).toBe(200);
@@ -232,7 +287,7 @@ describe('/asset', () => {
     it('should return stats of all assets neither favored nor archived', async () => {
       const { status, body } = await request(app)
         .get('/asset/statistics')
-        .set('Authorization', `Bearer ${userStats.accessToken}`)
+        .set('Authorization', `Bearer ${statsUser.accessToken}`)
         .query({ isFavorite: false, isArchived: false });
 
       expect(status).toBe(200);
@@ -488,6 +543,35 @@ describe('/asset', () => {
   });
 
   describe('POST /asset/upload', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).post(`/asset/upload`);
+      expect(body).toEqual(errorDto.unauthorized);
+      expect(status).toBe(401);
+    });
+
+    const invalid = [
+      { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
+      { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
+      { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
+      { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
+      { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
+      { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
+      { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
+      { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
+    ];
+
+    for (const { should, dto } of invalid) {
+      it(`should ${should}`, async () => {
+        const { status, body } = await request(app)
+          .post('/asset/upload')
+          .set('Authorization', `Bearer ${user1.accessToken}`)
+          .attach('assetData', makeRandomImage(), 'example.png')
+          .field(dto);
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest());
+      });
+    }
+
     const tests = [
       {
         input: 'formats/jpg/el_torcal_rocks.jpg',
@@ -601,7 +685,7 @@ describe('/asset', () => {
     ];
 
     for (const { input, expected } of tests) {
-      it(`should generate a thumbnail for ${input}`, async () => {
+      it(`should upload and generate a thumbnail for ${input}`, async () => {
         const filepath = join(testAssetDir, input);
         const { id, duplicate } = await utils.createAsset(admin.accessToken, {
           assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
@@ -631,6 +715,57 @@ describe('/asset', () => {
       expect(duplicate).toBe(true);
     });
 
+    it("should not upload to another user's library", async () => {
+      const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
+      const library = libraries.find((library) => library.ownerId === user1.userId) as LibraryResponseDto;
+
+      const { body, status } = await request(app)
+        .post('/asset/upload')
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .field('libraryId', library.id)
+        .field('deviceAssetId', 'example-image')
+        .field('deviceId', 'e2e')
+        .field('fileCreatedAt', new Date().toISOString())
+        .field('fileModifiedAt', new Date().toISOString())
+        .field('duration', '0:00:00.000000')
+        .attach('assetData', makeRandomImage(), 'example.png');
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest('Not found or no asset.upload access'));
+    });
+
+    it('should update the used quota', async () => {
+      const { body, status } = await request(app)
+        .post('/asset/upload')
+        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
+        .field('deviceAssetId', 'example-image')
+        .field('deviceId', 'e2e')
+        .field('fileCreatedAt', new Date().toISOString())
+        .field('fileModifiedAt', new Date().toISOString())
+        .attach('assetData', makeRandomImage(), 'example.jpg');
+
+      expect(body).toEqual({ id: expect.any(String), duplicate: false });
+      expect(status).toBe(201);
+
+      const { body: user } = await request(app).get('/user/me').set('Authorization', `Bearer ${quotaUser.accessToken}`);
+
+      expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
+    });
+
+    it('should not upload an asset if it would exceed the quota', async () => {
+      const { body, status } = await request(app)
+        .post('/asset/upload')
+        .set('Authorization', `Bearer ${quotaUser.accessToken}`)
+        .field('deviceAssetId', 'example-image')
+        .field('deviceId', 'e2e')
+        .field('fileCreatedAt', new Date().toISOString())
+        .field('fileModifiedAt', new Date().toISOString())
+        .attach('assetData', randomBytes(2014), 'example.jpg');
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest('Quota has been exceeded!'));
+    });
+
     // These hashes were created by copying the image files to a Samsung phone,
     // exporting the video from Samsung's stock Gallery app, and hashing them locally.
     // This ensures that immich+exiftool are extracting the videos the same way Samsung does.
@@ -675,7 +810,7 @@ describe('/asset', () => {
 
   describe('GET /asset/thumbnail/:id', () => {
     it('should require authentication', async () => {
-      const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
+      const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
 
       expect(status).toBe(401);
       expect(body).toEqual(errorDto.unauthorized);
@@ -683,12 +818,12 @@ describe('/asset', () => {
 
     it('should not include gps data for webp thumbnails', async () => {
       const { status, body, type } = await request(app)
-        .get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`)
+        .get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
         .set('Authorization', `Bearer ${admin.accessToken}`);
 
       await utils.waitForWebsocketEvent({
         event: 'upload',
-        assetId: assetLocation.id,
+        assetId: locationAsset.id,
       });
 
       expect(status).toBe(200);
@@ -702,7 +837,7 @@ describe('/asset', () => {
 
     it('should not include gps data for jpeg thumbnails', async () => {
       const { status, body, type } = await request(app)
-        .get(`/asset/thumbnail/${assetLocation.id}?format=JPEG`)
+        .get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
         .set('Authorization', `Bearer ${admin.accessToken}`);
 
       expect(status).toBe(200);
@@ -717,7 +852,7 @@ describe('/asset', () => {
 
   describe('GET /asset/file/:id', () => {
     it('should require authentication', async () => {
-      const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
+      const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
 
       expect(status).toBe(401);
       expect(body).toEqual(errorDto.unauthorized);
@@ -725,14 +860,14 @@ describe('/asset', () => {
 
     it('should download the original', async () => {
       const { status, body, type } = await request(app)
-        .get(`/asset/file/${assetLocation.id}`)
+        .get(`/asset/file/${locationAsset.id}`)
         .set('Authorization', `Bearer ${admin.accessToken}`);
 
       expect(status).toBe(200);
       expect(body).toBeDefined();
       expect(type).toBe('image/jpeg');
 
-      const asset = await utils.getAssetInfo(admin.accessToken, assetLocation.id);
+      const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
 
       const original = await readFile(locationAssetFilepath);
       const originalChecksum = utils.sha1(original);
@@ -742,4 +877,376 @@ describe('/asset', () => {
       expect(downloadChecksum).toBe(asset.checksum);
     });
   });
+
+  describe('GET /asset/map-marker', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/asset/map-marker');
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    // TODO archive one of these assets
+    it('should get map markers for all non-archived assets', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/map-marker')
+        .query({ isArchived: false })
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+
+      expect(status).toBe(200);
+      expect(body).toHaveLength(2);
+      expect(body).toEqual([
+        {
+          city: 'Palisade',
+          country: 'United States of America',
+          id: expect.any(String),
+          lat: expect.closeTo(39.115),
+          lon: expect.closeTo(-108.400_968),
+          state: 'Mesa County, Colorado',
+        },
+        {
+          city: 'Ralston',
+          country: 'United States of America',
+          id: expect.any(String),
+          lat: expect.closeTo(41.2203),
+          lon: expect.closeTo(-96.071_625),
+          state: 'Douglas County, Nebraska',
+        },
+      ]);
+    });
+
+    // TODO archive one of these assets
+    it('should get all map markers', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/map-marker')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+
+      expect(status).toBe(200);
+      expect(body).toEqual([
+        {
+          city: 'Palisade',
+          country: 'United States of America',
+          id: expect.any(String),
+          lat: expect.closeTo(39.115),
+          lon: expect.closeTo(-108.400_968),
+          state: 'Mesa County, Colorado',
+        },
+        {
+          city: 'Ralston',
+          country: 'United States of America',
+          id: expect.any(String),
+          lat: expect.closeTo(41.2203),
+          lon: expect.closeTo(-96.071_625),
+          state: 'Douglas County, Nebraska',
+        },
+      ]);
+    });
+  });
+
+  describe('GET /asset/time-buckets', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/asset/time-buckets').query({ size: TimeBucketSize.Month });
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should get time buckets by month', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Month });
+
+      expect(status).toBe(200);
+      expect(body).toEqual(
+        expect.arrayContaining([
+          { count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
+          { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
+        ]),
+      );
+    });
+
+    it('should not allow access for unrelated shared links', async () => {
+      const sharedLink = await utils.createSharedLink(user1.accessToken, {
+        type: SharedLinkType.Individual,
+        assetIds: user1Assets.map(({ id }) => id),
+      });
+
+      const { status, body } = await request(app)
+        .get('/asset/time-buckets')
+        .query({ key: sharedLink.key, size: TimeBucketSize.Month });
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.noPermission);
+    });
+
+    it('should get time buckets by day', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Day });
+
+      expect(status).toBe(200);
+      expect(body).toEqual([
+        { count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
+        { count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
+        { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
+      ]);
+    });
+  });
+
+  describe('GET /asset/time-bucket', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/asset/time-bucket').query({
+        size: TimeBucketSize.Month,
+        timeBucket: '1900-01-01T00:00:00.000Z',
+      });
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should handle 5 digit years', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/time-bucket')
+        .query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
+
+      expect(status).toBe(200);
+      expect(body).toEqual([]);
+    });
+
+    // TODO enable date string validation while still accepting 5 digit years
+    // it('should fail if time bucket is invalid', async () => {
+    //   const { status, body } = await request(app)
+    //     .get('/asset/time-bucket')
+    //     .set('Authorization', `Bearer ${user1.accessToken}`)
+    //     .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
+
+    //   expect(status).toBe(400);
+    //   expect(body).toEqual(errorDto.badRequest);
+    // });
+
+    it('should return time bucket', async () => {
+      const { status, body } = await request(app)
+        .get('/asset/time-bucket')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
+
+      expect(status).toBe(200);
+      expect(body).toEqual([]);
+    });
+
+    it('should return error if time bucket is requested with partners asset and archived', async () => {
+      const req1 = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
+
+      expect(req1.status).toBe(400);
+      expect(req1.body).toEqual(errorDto.badRequest());
+
+      const req2 = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
+
+      expect(req2.status).toBe(400);
+      expect(req2.body).toEqual(errorDto.badRequest());
+    });
+
+    it('should return error if time bucket is requested with partners asset and favorite', async () => {
+      const req1 = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
+
+      expect(req1.status).toBe(400);
+      expect(req1.body).toEqual(errorDto.badRequest());
+
+      const req2 = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
+        .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
+
+      expect(req2.status).toBe(400);
+      expect(req2.body).toEqual(errorDto.badRequest());
+    });
+
+    it('should return error if time bucket is requested with partners asset and trash', async () => {
+      const req = await request(app)
+        .get('/asset/time-buckets')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
+
+      expect(req.status).toBe(400);
+      expect(req.body).toEqual(errorDto.badRequest());
+    });
+  });
+
+  describe('GET /asset', () => {
+    it('should return stack data', async () => {
+      const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
+
+      const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id);
+
+      expect(status).toBe(200);
+      expect(stack).toEqual(
+        expect.objectContaining({
+          stackCount: 3,
+          stack:
+            // Response includes children at the root level
+            expect.arrayContaining([
+              expect.objectContaining({ id: stackAssets[1].id }),
+              expect.objectContaining({ id: stackAssets[2].id }),
+            ]),
+        }),
+      );
+    });
+  });
+
+  describe('PUT /asset', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).put('/asset');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should require a valid parent id', async () => {
+      const { status, body } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
+    });
+
+    it('should require access to the parent', async () => {
+      const { status, body } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.noPermission);
+    });
+
+    it('should add stack children', async () => {
+      const { status } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${stackUser.accessToken}`)
+        .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
+
+      expect(status).toBe(204);
+
+      const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
+      expect(asset.stack).not.toBeUndefined();
+      expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
+    });
+
+    it('should remove stack children', async () => {
+      const { status } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${stackUser.accessToken}`)
+        .send({ removeParent: true, ids: [stackAssets[1].id] });
+
+      expect(status).toBe(204);
+
+      const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
+      expect(asset.stack).not.toBeUndefined();
+      expect(asset.stack).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({ id: stackAssets[2].id }),
+          expect.objectContaining({ id: stackAssets[3].id }),
+        ]),
+      );
+    });
+
+    it('should remove all stack children', async () => {
+      const { status } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${stackUser.accessToken}`)
+        .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
+
+      expect(status).toBe(204);
+
+      const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
+      expect(asset.stack).toBeUndefined();
+    });
+
+    it('should merge stack children', async () => {
+      // create stack after previous test removed stack children
+      await updateAssets(
+        { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
+        { headers: asBearerAuth(stackUser.accessToken) },
+      );
+
+      const { status } = await request(app)
+        .put('/asset')
+        .set('Authorization', `Bearer ${stackUser.accessToken}`)
+        .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
+
+      expect(status).toBe(204);
+
+      const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
+      expect(asset.stack).not.toBeUndefined();
+      expect(asset.stack).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({ id: stackAssets[0].id }),
+          expect.objectContaining({ id: stackAssets[1].id }),
+          expect.objectContaining({ id: stackAssets[2].id }),
+        ]),
+      );
+    });
+  });
+
+  describe('PUT /asset/stack/parent', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).put('/asset/stack/parent');
+
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should require a valid id', async () => {
+      const { status, body } = await request(app)
+        .put('/asset/stack/parent')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.badRequest());
+    });
+
+    it('should require access', async () => {
+      const { status, body } = await request(app)
+        .put('/asset/stack/parent')
+        .set('Authorization', `Bearer ${user1.accessToken}`)
+        .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
+
+      expect(status).toBe(400);
+      expect(body).toEqual(errorDto.noPermission);
+    });
+
+    it('should make old parent child of new parent', async () => {
+      const { status } = await request(app)
+        .put('/asset/stack/parent')
+        .set('Authorization', `Bearer ${stackUser.accessToken}`)
+        .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
+
+      expect(status).toBe(200);
+
+      const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
+
+      // new parent
+      expect(asset.stack).not.toBeUndefined();
+      expect(asset.stack).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({ id: stackAssets[1].id }),
+          expect.objectContaining({ id: stackAssets[2].id }),
+          expect.objectContaining({ id: stackAssets[3].id }),
+        ]),
+      );
+    });
+  });
 });
diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts
index de7d9ef4c5..19b1b68073 100644
--- a/e2e/src/api/specs/search.e2e-spec.ts
+++ b/e2e/src/api/specs/search.e2e-spec.ts
@@ -1,52 +1,76 @@
-import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
+import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets } from '@immich/sdk';
+import { DateTime } from 'luxon';
 import { readFile } from 'node:fs/promises';
 import { join } from 'node:path';
 import { Socket } from 'socket.io-client';
 import { errorDto } from 'src/responses';
-import { app, testAssetDir, utils } from 'src/utils';
+import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
 import request from 'supertest';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
-const albums = { total: 0, count: 0, items: [], facets: [] };
+const today = DateTime.now();
 
 describe('/search', () => {
   let admin: LoginResponseDto;
+  let websocket: Socket;
+
   let assetFalcon: AssetFileUploadResponseDto;
   let assetDenali: AssetFileUploadResponseDto;
-  let websocket: Socket;
+  let assetCyclamen: AssetFileUploadResponseDto;
+  let assetNotocactus: AssetFileUploadResponseDto;
+  let assetSilver: AssetFileUploadResponseDto;
+  // let assetDensity: AssetFileUploadResponseDto;
+  // let assetPhiladelphia: AssetFileUploadResponseDto;
+  // let assetOrychophragmus: AssetFileUploadResponseDto;
+  // let assetRidge: AssetFileUploadResponseDto;
+  // let assetPolemonium: AssetFileUploadResponseDto;
+  // let assetWood: AssetFileUploadResponseDto;
+  let assetHeic: AssetFileUploadResponseDto;
+  let assetRocks: AssetFileUploadResponseDto;
+  let assetOneJpg6: AssetFileUploadResponseDto;
+  let assetOneHeic6: AssetFileUploadResponseDto;
+  let assetOneJpg5: AssetFileUploadResponseDto;
+  let assetGlarus: AssetFileUploadResponseDto;
+  let assetSprings: AssetFileUploadResponseDto;
+  let assetLast: AssetFileUploadResponseDto;
 
   beforeAll(async () => {
     await utils.resetDatabase();
     admin = await utils.adminSetup();
     websocket = await utils.connectWebsocket(admin.accessToken);
 
-    const files: string[] = [
-      '/albums/nature/prairie_falcon.jpg',
-      '/formats/webp/denali.webp',
-      '/formats/raw/Nikon/D700/philadelphia.nef',
-      '/albums/nature/orychophragmus_violaceus.jpg',
-      '/albums/nature/notocactus_minimus.jpg',
-      '/albums/nature/silver_fir.jpg',
-      '/albums/nature/tanners_ridge.jpg',
-      '/albums/nature/cyclamen_persicum.jpg',
-      '/albums/nature/polemonium_reptans.jpg',
-      '/albums/nature/wood_anemones.jpg',
-      '/formats/heic/IMG_2682.heic',
-      '/formats/jpg/el_torcal_rocks.jpg',
-      '/formats/png/density_plot.png',
-      '/formats/motionphoto/Samsung One UI 6.jpg',
-      '/formats/motionphoto/Samsung One UI 6.heic',
-      '/formats/motionphoto/Samsung One UI 5.jpg',
-      '/formats/raw/Nikon/D80/glarus.nef',
-      '/metadata/gps-position/thompson-springs.jpg',
+    const files = [
+      { filename: '/albums/nature/prairie_falcon.jpg' },
+      { filename: '/formats/webp/denali.webp' },
+      { filename: '/albums/nature/cyclamen_persicum.jpg', dto: { isFavorite: true } },
+      { filename: '/albums/nature/notocactus_minimus.jpg' },
+      { filename: '/albums/nature/silver_fir.jpg' },
+      { filename: '/formats/heic/IMG_2682.heic' },
+      { filename: '/formats/jpg/el_torcal_rocks.jpg' },
+      { filename: '/formats/motionphoto/Samsung One UI 6.jpg' },
+      { filename: '/formats/motionphoto/Samsung One UI 6.heic' },
+      { filename: '/formats/motionphoto/Samsung One UI 5.jpg' },
+      { filename: '/formats/raw/Nikon/D80/glarus.nef', dto: { isReadOnly: true } },
+      { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
+
+      // used for search suggestions
+      { filename: '/formats/png/density_plot.png' },
+      { filename: '/formats/raw/Nikon/D700/philadelphia.nef' },
+      { filename: '/albums/nature/orychophragmus_violaceus.jpg' },
+      { filename: '/albums/nature/tanners_ridge.jpg' },
+      { filename: '/albums/nature/polemonium_reptans.jpg' },
+
+      // last asset
+      { filename: '/albums/nature/wood_anemones.jpg' },
     ];
     const assets: AssetFileUploadResponseDto[] = [];
-    for (const filename of files) {
+    for (const { filename, dto } of files) {
       const bytes = await readFile(join(testAssetDir, filename));
       assets.push(
         await utils.createAsset(admin.accessToken, {
           deviceAssetId: `test-${filename}`,
           assetData: { bytes, filename },
+          ...dto,
         }),
       );
     }
@@ -55,7 +79,30 @@ describe('/search', () => {
       await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id });
     }
 
-    [assetFalcon, assetDenali] = assets;
+    [
+      assetFalcon,
+      assetDenali,
+      assetCyclamen,
+      assetNotocactus,
+      assetSilver,
+      assetHeic,
+      assetRocks,
+      assetOneJpg6,
+      assetOneHeic6,
+      assetOneJpg5,
+      assetGlarus,
+      assetSprings,
+      // assetDensity,
+      // assetPhiladelphia,
+      // assetOrychophragmus,
+      // assetRidge,
+      // assetPolemonium,
+      // assetWood,
+    ] = assets;
+
+    assetLast = assets.at(-1) as AssetFileUploadResponseDto;
+
+    await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
   });
 
   afterAll(async () => {
@@ -69,44 +116,226 @@ describe('/search', () => {
       expect(body).toEqual(errorDto.unauthorized);
     });
 
-    it('should search by camera make', async () => {
-      const { status, body } = await request(app)
-        .post('/search/metadata')
-        .set('Authorization', `Bearer ${admin.accessToken}`)
-        .send({ make: 'Canon' });
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        albums,
-        assets: {
-          count: 2,
-          items: expect.arrayContaining([
-            expect.objectContaining({ id: assetDenali.id }),
-            expect.objectContaining({ id: assetFalcon.id }),
-          ]),
-          facets: [],
-          nextPage: null,
-          total: 2,
-        },
-      });
-    });
+    const badTests = [
+      {
+        should: 'should reject page as a string',
+        dto: { page: 'abc' },
+        expected: ['page must not be less than 1', 'page must be an integer number'],
+      },
+      {
+        should: 'should reject page as a decimal',
+        dto: { page: 1.5 },
+        expected: ['page must be an integer number'],
+      },
+      {
+        should: 'should reject page as a negative number',
+        dto: { page: -10 },
+        expected: ['page must not be less than 1'],
+      },
+      {
+        should: 'should reject page as 0',
+        dto: { page: 0 },
+        expected: ['page must not be less than 1'],
+      },
+      {
+        should: 'should reject size as a string',
+        dto: { size: 'abc' },
+        expected: [
+          'size must not be greater than 1000',
+          'size must not be less than 1',
+          'size must be an integer number',
+        ],
+      },
+      {
+        should: 'should reject an invalid size',
+        dto: { size: -1.5 },
+        expected: ['size must not be less than 1', 'size must be an integer number'],
+      },
+      ...[
+        'isArchived',
+        'isFavorite',
+        'isReadOnly',
+        'isExternal',
+        'isEncoded',
+        'isMotion',
+        'isOffline',
+        'isVisible',
+      ].map((value) => ({
+        should: `should reject ${value} not a boolean`,
+        dto: { [value]: 'immich' },
+        expected: [`${value} must be a boolean value`],
+      })),
+    ];
 
-    it('should search by camera model', async () => {
-      const { status, body } = await request(app)
-        .post('/search/metadata')
-        .set('Authorization', `Bearer ${admin.accessToken}`)
-        .send({ model: 'Canon EOS 7D' });
-      expect(status).toBe(200);
-      expect(body).toEqual({
-        albums,
-        assets: {
-          count: 1,
-          items: [expect.objectContaining({ id: assetDenali.id })],
-          facets: [],
-          nextPage: null,
-          total: 1,
-        },
+    for (const { should, dto, expected } of badTests) {
+      it(should, async () => {
+        const { status, body } = await request(app)
+          .post('/search/metadata')
+          .set('Authorization', `Bearer ${admin.accessToken}`)
+          .send(dto);
+        expect(status).toBe(400);
+        expect(body).toEqual(errorDto.badRequest(expected));
       });
-    });
+    }
+
+    const searchTests = [
+      {
+        should: 'should get my assets',
+        deferred: () => ({ dto: { size: 1 }, assets: [assetLast] }),
+      },
+      {
+        should: 'should sort my assets in reverse',
+        deferred: () => ({ dto: { order: 'asc', size: 2 }, assets: [assetCyclamen, assetNotocactus] }),
+      },
+      {
+        should: 'should support pagination',
+        deferred: () => ({ dto: { order: 'asc', size: 1, page: 2 }, assets: [assetNotocactus] }),
+      },
+      {
+        should: 'should search by checksum (base64)',
+        deferred: () => ({ dto: { checksum: '9IXBDMjj9OrQb+1YMHprZJgZ/UQ=' }, assets: [assetCyclamen] }),
+      },
+      {
+        should: 'should search by checksum (hex)',
+        deferred: () => ({ dto: { checksum: 'f485c10cc8e3f4ead06fed58307a6b649819fd44' }, assets: [assetCyclamen] }),
+      },
+      { should: 'should search by id', deferred: () => ({ dto: { id: assetCyclamen.id }, assets: [assetCyclamen] }) },
+      {
+        should: 'should search by isFavorite (true)',
+        deferred: () => ({ dto: { isFavorite: true }, assets: [assetCyclamen] }),
+      },
+      {
+        should: 'should search by isFavorite (false)',
+        deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
+      },
+      {
+        should: 'should search by isArchived (true)',
+        deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
+      },
+      {
+        should: 'should search by isArchived (false)',
+        deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
+      },
+      {
+        should: 'should search by isReadOnly (true)',
+        deferred: () => ({ dto: { isReadOnly: true }, assets: [assetGlarus] }),
+      },
+      {
+        should: 'should search by isReadOnly (false)',
+        deferred: () => ({ dto: { size: 1, isReadOnly: false }, assets: [assetLast] }),
+      },
+      {
+        should: 'should search by type (image)',
+        deferred: () => ({ dto: { size: 1, type: 'IMAGE' }, assets: [assetLast] }),
+      },
+      {
+        should: 'should search by type (video)',
+        deferred: () => ({
+          dto: { type: 'VIDEO' },
+          assets: [
+            // the three live motion photos
+            { id: expect.any(String) },
+            { id: expect.any(String) },
+            { id: expect.any(String) },
+          ],
+        }),
+      },
+      {
+        should: 'should search by trashedBefore',
+        deferred: () => ({ dto: { trashedBefore: today.plus({ hour: 1 }).toJSDate() }, assets: [assetSilver] }),
+      },
+      {
+        should: 'should search by trashedBefore (no results)',
+        deferred: () => ({ dto: { trashedBefore: today.minus({ days: 1 }).toJSDate() }, assets: [] }),
+      },
+      {
+        should: 'should search by trashedAfter',
+        deferred: () => ({ dto: { trashedAfter: today.minus({ hour: 1 }).toJSDate() }, assets: [assetSilver] }),
+      },
+      {
+        should: 'should search by trashedAfter (no results)',
+        deferred: () => ({ dto: { trashedAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
+      },
+      {
+        should: 'should search by takenBefore',
+        deferred: () => ({ dto: { size: 1, takenBefore: today.plus({ hour: 1 }).toJSDate() }, assets: [assetLast] }),
+      },
+      {
+        should: 'should search by takenBefore (no results)',
+        deferred: () => ({ dto: { takenBefore: DateTime.fromObject({ year: 1234 }).toJSDate() }, assets: [] }),
+      },
+      {
+        should: 'should search by takenAfter',
+        deferred: () => ({
+          dto: { size: 1, takenAfter: DateTime.fromObject({ year: 1234 }).toJSDate() },
+          assets: [assetLast],
+        }),
+      },
+      {
+        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: () => ({
+          dto: { originalFileName: 'rocks' },
+          assets: [assetRocks],
+        }),
+      },
+      {
+        should: 'should search by originalFilename with spaces',
+        deferred: () => ({
+          dto: { originalFileName: 'Samsung One', type: 'IMAGE' },
+          assets: [assetOneJpg5, assetOneJpg6, assetOneHeic6],
+        }),
+      },
+      {
+        should: 'should search by city',
+        deferred: () => ({ dto: { city: 'Ralston' }, assets: [assetHeic] }),
+      },
+      {
+        should: 'should search by state',
+        deferred: () => ({ dto: { state: 'Douglas County, Nebraska' }, assets: [assetHeic] }),
+      },
+      {
+        should: 'should search by country',
+        deferred: () => ({ dto: { country: 'United States of America' }, assets: [assetHeic] }),
+      },
+      {
+        should: 'should search by make',
+        deferred: () => ({ dto: { make: 'Canon' }, assets: [assetFalcon, assetDenali] }),
+      },
+      {
+        should: 'should search by model',
+        deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }),
+      },
+    ];
+
+    for (const { should, deferred } of searchTests) {
+      it(should, async () => {
+        const { assets, dto } = deferred();
+        const { status, body } = await request(app)
+          .post('/search/metadata')
+          .send(dto)
+          .set('Authorization', `Bearer ${admin.accessToken}`);
+        console.dir({ status, body }, { depth: 10 });
+        expect(status).toBe(200);
+        expect(body.assets).toBeDefined();
+        expect(Array.isArray(body.assets.items)).toBe(true);
+        console.log({ assets: body.assets.items });
+        for (const [i, asset] of assets.entries()) {
+          expect(body.assets.items[i]).toEqual(expect.objectContaining({ id: asset.id }));
+        }
+        expect(body.assets.items).toHaveLength(assets.length);
+      });
+    }
   });
 
   describe('POST /search/smart', () => {
diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts
index 6a1a1b3968..56070e6e34 100644
--- a/e2e/src/fixtures.ts
+++ b/e2e/src/fixtures.ts
@@ -21,6 +21,13 @@ export const signupDto = {
 };
 
 export const createUserDto = {
+  create(key: string) {
+    return {
+      email: `${key}@immich.cloud`,
+      name: `User ${key}`,
+      password: `password-${key}`,
+    };
+  },
   user1: {
     email: 'user1@immich.cloud',
     name: 'User 1',
@@ -36,6 +43,12 @@ export const createUserDto = {
     name: 'User 3',
     password: 'password123',
   },
+  userQuota: {
+    email: 'user-quota@immich.cloud',
+    name: 'User Quota',
+    password: 'password-quota',
+    quotaSizeInBytes: 512,
+  },
 };
 
 export const userDto = {
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index d62497b8e4..af86a608db 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -104,6 +104,8 @@ export const utils = {
       }
 
       tables = tables || [
+        // TODO e2e test for deleting a stack, since it is quite complex
+        'asset_stack',
         'libraries',
         'shared_links',
         'person',
@@ -117,9 +119,17 @@ export const utils = {
         'system_metadata',
       ];
 
-      for (const table of tables) {
-        await client.query(`DELETE FROM ${table} CASCADE;`);
+      const sql: string[] = [];
+
+      if (tables.includes('asset_stack')) {
+        sql.push('UPDATE "assets" SET "stackId" = NULL;');
       }
+
+      for (const table of tables) {
+        sql.push(`DELETE FROM ${table} CASCADE;`);
+      }
+
+      await client.query(sql.join('\n'));
     } catch (error) {
       console.error('Failed to reset database', error);
       throw error;
diff --git a/server/e2e/api/jest-e2e.json b/server/e2e/api/jest-e2e.json
deleted file mode 100644
index 9fd67774f3..0000000000
--- a/server/e2e/api/jest-e2e.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "moduleFileExtensions": ["js", "json", "ts"],
-  "modulePaths": ["<rootDir>"],
-  "rootDir": "../..",
-  "globalSetup": "<rootDir>/e2e/api/setup.ts",
-  "testEnvironment": "node",
-  "testMatch": ["**/e2e/api/specs/*.e2e-spec.[tj]s"],
-  "transform": {
-    "^.+\\.(t|j)s$": "ts-jest"
-  },
-  "collectCoverageFrom": [
-    "<rootDir>/src/**/*.(t|j)s",
-    "!<rootDir>/src/**/*.spec.(t|s)s",
-    "!<rootDir>/src/infra/migrations/**"
-  ],
-  "coverageDirectory": "./coverage",
-  "moduleNameMapper": {
-    "^@test(|/.*)$": "<rootDir>/test/$1",
-    "^@app/immich(|/.*)$": "<rootDir>/src/immich/$1",
-    "^@app/infra(|/.*)$": "<rootDir>/src/infra/$1",
-    "^@app/domain(|/.*)$": "<rootDir>/src/domain/$1"
-  }
-}
diff --git a/server/e2e/api/setup.ts b/server/e2e/api/setup.ts
deleted file mode 100644
index 88f2f598bd..0000000000
--- a/server/e2e/api/setup.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { PostgreSqlContainer } from '@testcontainers/postgresql';
-import path from 'node:path';
-
-export default async () => {
-  let IMMICH_TEST_ASSET_PATH: string = '';
-
-  if (process.env.IMMICH_TEST_ASSET_PATH === undefined) {
-    IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../test/assets/`);
-    process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH;
-  } else {
-    IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
-  }
-
-  const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
-    .withDatabase('immich')
-    .withUsername('postgres')
-    .withPassword('postgres')
-    .withReuse()
-    .withCommand(['-c', 'fsync=off', '-c', 'shared_preload_libraries=vectors.so'])
-    .start();
-
-  process.env.DB_URL = pg.getConnectionUri();
-  process.env.NODE_ENV = 'development';
-  process.env.TZ = 'Z';
-
-  if (process.env.LOG_LEVEL === undefined) {
-    process.env.LOG_LEVEL = 'fatal';
-  }
-};
diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts
deleted file mode 100644
index 6badd4c674..0000000000
--- a/server/e2e/api/specs/asset.e2e-spec.ts
+++ /dev/null
@@ -1,1151 +0,0 @@
-import {
-  AssetResponseDto,
-  IAssetRepository,
-  IPersonRepository,
-  LibraryResponseDto,
-  LoginResponseDto,
-  TimeBucketSize,
-  WithoutProperty,
-  mapAsset,
-  usePagination,
-} from '@app/domain';
-import { AssetController } from '@app/immich';
-import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/infra/entities';
-import { AssetRepository } from '@app/infra/repositories';
-import { INestApplication } from '@nestjs/common';
-import { errorStub, userDto, uuidStub } from '@test/fixtures';
-import { assetApi } from 'e2e/client/asset-api';
-import { randomBytes } from 'node:crypto';
-import request from 'supertest';
-import { api } from '../../client';
-import { generateAsset, testApp, today, yesterday } from '../utils';
-
-const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
-  const dto: Record<string, any> = {
-    deviceAssetId: 'example-image',
-    deviceId: 'TEST',
-    fileCreatedAt: new Date().toISOString(),
-    fileModifiedAt: new Date().toISOString(),
-    isFavorite: 'testing',
-    duration: '0:00:00.000000',
-  };
-
-  const omit = options?.omit;
-  if (omit) {
-    delete dto[omit];
-  }
-
-  return dto;
-};
-
-describe(`${AssetController.name} (e2e)`, () => {
-  let app: INestApplication;
-  let server: any;
-  let assetRepository: IAssetRepository;
-  let admin: LoginResponseDto;
-  let user1: LoginResponseDto;
-  let user2: LoginResponseDto;
-  let userWithQuota: LoginResponseDto;
-  let libraries: LibraryResponseDto[];
-  let asset1: AssetResponseDto;
-  let asset2: AssetResponseDto;
-  let asset3: AssetResponseDto;
-  let asset4: AssetResponseDto;
-  let asset5: AssetResponseDto;
-  let asset6: AssetResponseDto;
-
-  const createAsset = async (
-    loginResponse: LoginResponseDto,
-    fileCreatedAt: Date,
-    other: Partial<AssetEntity> = {},
-  ) => {
-    const asset = await assetRepository.create(
-      generateAsset(loginResponse.userId, libraries, { fileCreatedAt, ...other }),
-    );
-
-    return mapAsset(asset);
-  };
-
-  beforeAll(async () => {
-    app = await testApp.create();
-    server = app.getHttpServer();
-    assetRepository = app.get<IAssetRepository>(IAssetRepository);
-
-    await testApp.reset();
-
-    await api.authApi.adminSignUp(server);
-    admin = await api.authApi.adminLogin(server);
-
-    await Promise.all([
-      api.userApi.create(server, admin.accessToken, userDto.user1),
-      api.userApi.create(server, admin.accessToken, userDto.user2),
-      api.userApi.create(server, admin.accessToken, userDto.userWithQuota),
-    ]);
-
-    [user1, user2, userWithQuota] = await Promise.all([
-      api.authApi.login(server, userDto.user1),
-      api.authApi.login(server, userDto.user2),
-      api.authApi.login(server, userDto.userWithQuota),
-    ]);
-
-    libraries = await api.libraryApi.getAll(server, admin.accessToken);
-  });
-
-  beforeEach(async () => {
-    await testApp.reset({ entities: [AssetEntity, AssetStackEntity] });
-
-    [asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([
-      createAsset(user1, new Date('1970-01-01')),
-      createAsset(user1, new Date('1970-02-10')),
-      createAsset(user1, new Date('1970-02-11'), {
-        isFavorite: true,
-        isExternal: true,
-        isReadOnly: true,
-        type: AssetType.VIDEO,
-        fileCreatedAt: yesterday.toJSDate(),
-        fileModifiedAt: yesterday.toJSDate(),
-        createdAt: yesterday.toJSDate(),
-        updatedAt: yesterday.toJSDate(),
-        localDateTime: yesterday.toJSDate(),
-        encodedVideoPath: '/path/to/encoded-video.mp4',
-        webpPath: '/path/to/thumb.webp',
-        resizePath: '/path/to/thumb.jpg',
-      }),
-      createAsset(user2, new Date('1970-01-01')),
-      createAsset(user1, new Date('1970-01-01'), {
-        deletedAt: yesterday.toJSDate(),
-      }),
-      createAsset(user1, new Date('1970-02-11'), {
-        isArchived: true,
-      }),
-    ]);
-
-    await assetRepository.upsertExif({
-      assetId: asset3.id,
-      latitude: 90,
-      longitude: 90,
-      city: 'Immich',
-      state: 'Nebraska',
-      country: 'United States',
-      make: 'Cannon',
-      model: 'EOS Rebel T7',
-      lensModel: 'Fancy lens',
-    });
-  });
-
-  afterAll(async () => {
-    await testApp.teardown();
-  });
-
-  describe('GET /assets', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get('/assets');
-      expect(body).toEqual(errorStub.unauthorized);
-      expect(status).toBe(401);
-    });
-
-    const badTests = [
-      //
-      {
-        should: 'should reject page as a string',
-        query: { page: 'abc' },
-        expected: ['page must not be less than 1', 'page must be an integer number'],
-      },
-      {
-        should: 'should reject page as a decimal',
-        query: { page: 1.5 },
-        expected: ['page must be an integer number'],
-      },
-      {
-        should: 'should reject page as a negative number',
-        query: { page: -10 },
-        expected: ['page must not be less than 1'],
-      },
-      {
-        should: 'should reject page as 0',
-        query: { page: 0 },
-        expected: ['page must not be less than 1'],
-      },
-      {
-        should: 'should reject size as a string',
-        query: { size: 'abc' },
-        expected: [
-          'size must not be greater than 1000',
-          'size must not be less than 1',
-          'size must be an integer number',
-        ],
-      },
-      {
-        should: 'should reject an invalid size',
-        query: { size: -1.5 },
-        expected: ['size must not be less than 1', 'size must be an integer number'],
-      },
-      ...[
-        'isArchived',
-        'isFavorite',
-        'isReadOnly',
-        'isExternal',
-        'isEncoded',
-        'isMotion',
-        'isOffline',
-        'isVisible',
-      ].map((value) => ({
-        should: `should reject ${value} not a boolean`,
-        query: { [value]: 'immich' },
-        expected: [`${value} must be a boolean value`],
-      })),
-    ];
-
-    for (const { should, query, expected } of badTests) {
-      it(should, async () => {
-        const { status, body } = await request(server)
-          .get('/assets')
-          .set('Authorization', `Bearer ${user1.accessToken}`)
-          .query(query);
-        expect(status).toBe(400);
-        expect(body).toEqual(errorStub.badRequest(expected));
-      });
-    }
-
-    const searchTests = [
-      {
-        should: 'should only return my own assets',
-        deferred: () => ({
-          query: {},
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should sort my assets in reverse',
-        deferred: () => ({
-          query: { order: 'asc' },
-          assets: [asset1, asset2, asset3],
-        }),
-      },
-      {
-        should: 'should support custom page sizes',
-        deferred: () => ({
-          query: { size: 1 },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should support pagination',
-        deferred: () => ({
-          query: { size: 1, page: 2 },
-          assets: [asset2],
-        }),
-      },
-      {
-        should: 'should search by checksum (base64)',
-        deferred: () => ({
-          query: { checksum: asset1.checksum },
-          assets: [asset1],
-        }),
-      },
-      {
-        should: 'should search by checksum (hex)',
-        deferred: () => ({
-          query: { checksum: Buffer.from(asset1.checksum, 'base64').toString('hex') },
-          assets: [asset1],
-        }),
-      },
-      {
-        should: 'should search by id',
-        deferred: () => ({
-          query: { id: asset1.id },
-          assets: [asset1],
-        }),
-      },
-      {
-        should: 'should search by isFavorite (true)',
-        deferred: () => ({
-          query: { isFavorite: true },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by isFavorite (false)',
-        deferred: () => ({
-          query: { isFavorite: false },
-          assets: [asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by isArchived (true)',
-        deferred: () => ({
-          query: { isArchived: true },
-          assets: [asset6],
-        }),
-      },
-      {
-        should: 'should search by isArchived (false)',
-        deferred: () => ({
-          query: { isArchived: false },
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by isReadOnly (true)',
-        deferred: () => ({
-          query: { isReadOnly: true },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by isReadOnly (false)',
-        deferred: () => ({
-          query: { isReadOnly: false },
-          assets: [asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by type (image)',
-        deferred: () => ({
-          query: { type: 'IMAGE' },
-          assets: [asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by type (video)',
-        deferred: () => ({
-          query: { type: 'VIDEO' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by withArchived (true)',
-        deferred: () => ({
-          query: { withArchived: true },
-          assets: [asset3, asset6, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by withArchived (false)',
-        deferred: () => ({
-          query: { withArchived: false },
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by createdBefore',
-        deferred: () => ({
-          query: { createdBefore: yesterday.plus({ hour: 1 }).toJSDate() },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by createdBefore (no results)',
-        deferred: () => ({
-          query: { createdBefore: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by createdAfter',
-        deferred: () => ({
-          query: { createdAfter: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by createdAfter (no results)',
-        deferred: () => ({
-          query: { createdAfter: today.plus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by updatedBefore',
-        deferred: () => ({
-          query: { updatedBefore: yesterday.plus({ hour: 1 }).toJSDate() },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by updatedBefore (no results)',
-        deferred: () => ({
-          query: { updatedBefore: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by updatedAfter',
-        deferred: () => ({
-          query: { updatedAfter: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by updatedAfter (no results)',
-        deferred: () => ({
-          query: { updatedAfter: today.plus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by trashedBefore',
-        deferred: () => ({
-          query: { trashedBefore: yesterday.plus({ hour: 1 }).toJSDate() },
-          assets: [asset5],
-        }),
-      },
-      {
-        should: 'should search by trashedBefore (no results)',
-        deferred: () => ({
-          query: { trashedBefore: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by trashedAfter',
-        deferred: () => ({
-          query: { trashedAfter: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [asset5],
-        }),
-      },
-      {
-        should: 'should search by trashedAfter (no results)',
-        deferred: () => ({
-          query: { trashedAfter: today.plus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by takenBefore',
-        deferred: () => ({
-          query: { takenBefore: yesterday.plus({ hour: 1 }).toJSDate() },
-          assets: [asset3, asset2, asset1],
-        }),
-      },
-      {
-        should: 'should search by takenBefore (no results)',
-        deferred: () => ({
-          query: { takenBefore: yesterday.minus({ years: 100 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by takenAfter',
-        deferred: () => ({
-          query: { takenAfter: yesterday.minus({ hour: 1 }).toJSDate() },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by takenAfter (no results)',
-        deferred: () => ({
-          query: { takenAfter: today.plus({ hour: 1 }).toJSDate() },
-          assets: [],
-        }),
-      },
-      {
-        should: 'should search by originalPath',
-        deferred: () => ({
-          query: { originalPath: asset1.originalPath },
-          assets: [asset1],
-        }),
-      },
-      {
-        should: 'should search by originalFilename',
-        deferred: () => ({
-          query: { originalFileName: asset1.originalFileName },
-          assets: [asset1],
-        }),
-      },
-      {
-        should: 'should search by encodedVideoPath',
-        deferred: () => ({
-          query: { encodedVideoPath: '/path/to/encoded-video.mp4' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by resizePath',
-        deferred: () => ({
-          query: { resizePath: '/path/to/thumb.jpg' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by webpPath',
-        deferred: () => ({
-          query: { webpPath: '/path/to/thumb.webp' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by city',
-        deferred: () => ({
-          query: { city: 'Immich' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by state',
-        deferred: () => ({
-          query: { state: 'Nebraska' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by country',
-        deferred: () => ({
-          query: { country: 'United States' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by make',
-        deferred: () => ({
-          query: { make: 'Cannon' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by country',
-        deferred: () => ({
-          query: { model: 'EOS Rebel T7' },
-          assets: [asset3],
-        }),
-      },
-      {
-        should: 'should search by lensModel',
-        deferred: () => ({
-          query: { lensModel: 'Fancy lens' },
-          assets: [asset3],
-        }),
-      },
-    ];
-
-    for (const { should, deferred } of searchTests) {
-      it(should, async () => {
-        const { assets, query } = deferred();
-        const { status, body } = await request(server)
-          .get('/assets')
-          .query(query)
-          .set('Authorization', `Bearer ${user1.accessToken}`);
-
-        expect(status).toBe(200);
-        expect(body.length).toBe(assets.length);
-        for (const [i, asset] of assets.entries()) {
-          expect(body[i]).toEqual(expect.objectContaining({ id: asset.id }));
-        }
-      });
-    }
-
-    it('should return stack data', async () => {
-      const parentId = asset1.id;
-      const childIds = [asset2.id, asset3.id];
-      await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: parentId, ids: childIds });
-
-      const body = await assetApi.getAllAssets(server, user1.accessToken);
-      // Response includes parent with stack children count
-      const parentDto = body.find((a) => a.id == parentId);
-      expect(parentDto?.stackCount).toEqual(3);
-
-      // Response includes children at the root level
-      expect.arrayContaining([expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })]);
-    });
-  });
-
-  describe('POST /asset/upload', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(server)
-        .post(`/asset/upload`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', false)
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', randomBytes(32), 'example.jpg');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    const invalid = [
-      { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
-      { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
-      { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
-      { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
-      { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
-      { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
-      { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
-      { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
-    ];
-
-    for (const { should, dto } of invalid) {
-      it(`should ${should}`, async () => {
-        const { status, body } = await request(server)
-          .post('/asset/upload')
-          .set('Authorization', `Bearer ${user1.accessToken}`)
-          .attach('assetData', randomBytes(32), 'example.jpg')
-          .field(dto);
-        expect(status).toBe(400);
-        expect(body).toEqual(errorStub.badRequest());
-      });
-    }
-
-    it('should upload a new asset', async () => {
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', 'true')
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', randomBytes(32), 'example.jpg');
-      expect(status).toBe(201);
-      expect(body).toEqual({ id: expect.any(String), duplicate: false });
-
-      const asset = await api.assetApi.get(server, user1.accessToken, body.id);
-      expect(asset).toMatchObject({ id: body.id, isFavorite: true });
-    });
-
-    it('should have correct original file name and extension (simple)', async () => {
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', 'true')
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', randomBytes(32), 'example.jpg');
-      expect(status).toBe(201);
-      expect(body).toEqual({ id: expect.any(String), duplicate: false });
-
-      const asset = await api.assetApi.get(server, user1.accessToken, body.id);
-      expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.jpg' });
-    });
-
-    it('should have correct original file name and extension (complex)', async () => {
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', 'true')
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', randomBytes(32), 'example.complex.ext.jpg');
-      expect(status).toBe(201);
-      expect(body).toEqual({ id: expect.any(String), duplicate: false });
-
-      const asset = await api.assetApi.get(server, user1.accessToken, body.id);
-      expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.complex.ext.jpg' });
-    });
-
-    it('should not upload the same asset twice', async () => {
-      const content = randomBytes(32);
-      await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', false)
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', content, 'example.jpg');
-
-      expect(status).toBe(200);
-      expect(body.duplicate).toBe(true);
-    });
-
-    it("should not upload to another user's library", async () => {
-      const content = randomBytes(32);
-      const [library] = await api.libraryApi.getAll(server, admin.accessToken);
-      await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
-
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .field('libraryId', library.id)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', false)
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', content, 'example.jpg');
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest('Not found or no asset.upload access'));
-    });
-
-    it('should update the used quota', async () => {
-      const content = randomBytes(32);
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${userWithQuota.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', 'true')
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', content, 'example.jpg');
-
-      expect(status).toBe(201);
-      expect(body).toEqual({ id: expect.any(String), duplicate: false });
-
-      const { body: user } = await request(server)
-        .get('/user/me')
-        .set('Authorization', `Bearer ${userWithQuota.accessToken}`);
-
-      expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 32 }));
-    });
-
-    it('should not upload an asset if it would exceed the quota', async () => {
-      const content = randomBytes(420);
-      const { body, status } = await request(server)
-        .post('/asset/upload')
-        .set('Authorization', `Bearer ${userWithQuota.accessToken}`)
-        .field('deviceAssetId', 'example-image')
-        .field('deviceId', 'TEST')
-        .field('fileCreatedAt', new Date().toISOString())
-        .field('fileModifiedAt', new Date().toISOString())
-        .field('isFavorite', 'true')
-        .field('duration', '0:00:00.000000')
-        .attach('assetData', content, 'example.jpg');
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest('Quota has been exceeded!'));
-    });
-  });
-
-  describe('GET /asset/time-buckets', () => {
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should get time buckets by month', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH });
-
-      expect(status).toBe(200);
-      expect(body).toEqual(
-        expect.arrayContaining([
-          { count: 1, timeBucket: '2023-11-01T00:00:00.000Z' },
-          { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
-          { count: 2, timeBucket: '1970-02-01T00:00:00.000Z' },
-        ]),
-      );
-    });
-
-    it('should not allow access for unrelated shared links', async () => {
-      const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
-        type: SharedLinkType.INDIVIDUAL,
-        assetIds: [asset1.id, asset2.id],
-      });
-
-      const { status, body } = await request(server)
-        .get('/asset/time-buckets')
-        .query({ key: sharedLink.key, size: TimeBucketSize.MONTH });
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.noPermission);
-    });
-
-    it('should get time buckets by day', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.DAY });
-
-      expect(status).toBe(200);
-      expect(body).toEqual(
-        expect.arrayContaining([
-          { count: 1, timeBucket: asset1.fileCreatedAt.toISOString() },
-          { count: 1, timeBucket: asset2.fileCreatedAt.toISOString() },
-          { count: 1, timeBucket: asset3.fileCreatedAt.toISOString() },
-        ]),
-      );
-    });
-  });
-
-  describe('GET /asset/time-bucket', () => {
-    let timeBucket: string;
-    beforeEach(async () => {
-      const { body, status } = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH });
-
-      expect(status).toBe(200);
-      timeBucket = body[1].timeBucket;
-    });
-
-    it('should require authentication', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/time-bucket')
-        .query({ size: TimeBucketSize.MONTH, timeBucket });
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should handle 5 digit years', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/time-bucket')
-        .query({ size: TimeBucketSize.MONTH, timeBucket: '+012345-01-01T00:00:00.000Z' })
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(status).toBe(200);
-      expect(body).toEqual([]);
-    });
-
-    // it('should fail if time bucket is invalid', async () => {
-    //   const { status, body } = await request(server)
-    //     .get('/asset/time-bucket')
-    //     .set('Authorization', `Bearer ${user1.accessToken}`)
-    //     .query({ size: TimeBucketSize.MONTH, timeBucket: 'foo' });
-
-    //   expect(status).toBe(400);
-    //   expect(body).toEqual(errorStub.badRequest);
-    // });
-
-    it('should return time bucket', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/time-bucket')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, timeBucket });
-
-      expect(status).toBe(200);
-      expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset2.id })]));
-    });
-
-    it('should return error if time bucket is requested with partners asset and archived', async () => {
-      const req1 = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: true });
-
-      expect(req1.status).toBe(400);
-      expect(req1.body).toEqual(errorStub.badRequest());
-
-      const req2 = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: undefined });
-
-      expect(req2.status).toBe(400);
-      expect(req2.body).toEqual(errorStub.badRequest());
-    });
-
-    it('should return error if time bucket is requested with partners asset and favorite', async () => {
-      const req1 = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: true });
-
-      expect(req1.status).toBe(400);
-      expect(req1.body).toEqual(errorStub.badRequest());
-
-      const req2 = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: false });
-
-      expect(req2.status).toBe(400);
-      expect(req2.body).toEqual(errorStub.badRequest());
-    });
-
-    it('should return error if time bucket is requested with partners asset and trash', async () => {
-      const req = await request(server)
-        .get('/asset/time-buckets')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ size: TimeBucketSize.MONTH, withPartners: true, isTrashed: true });
-
-      expect(req.status).toBe(400);
-      expect(req.body).toEqual(errorStub.badRequest());
-    });
-  });
-
-  describe('GET /asset/map-marker', () => {
-    beforeEach(async () => {
-      await Promise.all([
-        assetRepository.save({ id: asset1.id, isArchived: true }),
-        assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }),
-        assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }),
-      ]);
-    });
-
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).get('/asset/map-marker');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should get map markers for all non-archived assets', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/map-marker')
-        .query({ isArchived: false })
-        .set('Authorization', `Bearer ${user1.accessToken}`);
-
-      expect(status).toBe(200);
-      expect(body).toHaveLength(2);
-      expect(body).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({ id: asset2.id }),
-          expect.objectContaining({ id: asset3.id }),
-        ]),
-      );
-    });
-
-    it('should get all map markers', async () => {
-      const { status, body } = await request(server)
-        .get('/asset/map-marker')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .query({ isArchived: false });
-
-      expect(status).toBe(200);
-      expect(body).toHaveLength(2);
-      expect(body).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({ id: asset2.id }),
-          expect.objectContaining({ id: asset3.id }),
-        ]),
-      );
-    });
-  });
-
-  describe('PUT /asset', () => {
-    beforeEach(async () => {
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: asset1.id, ids: [asset2.id, asset3.id] });
-
-      expect(status).toBe(204);
-    });
-
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).put('/asset');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should require a valid parent id', async () => {
-      const { status, body } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: uuidStub.invalid, ids: [asset1.id] });
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest(['stackParentId must be a UUID']));
-    });
-
-    it('should require access to the parent', async () => {
-      const { status, body } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: asset4.id, ids: [asset1.id] });
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.noPermission);
-    });
-
-    it('should add stack children', async () => {
-      const [parent, child] = await Promise.all([
-        createAsset(user1, new Date('1970-01-01')),
-        createAsset(user1, new Date('1970-01-01')),
-      ]);
-
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: parent.id, ids: [child.id] });
-
-      expect(status).toBe(204);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, parent.id);
-      expect(asset.stack).not.toBeUndefined();
-      expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: child.id })]));
-    });
-
-    it('should remove stack children', async () => {
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ removeParent: true, ids: [asset2.id] });
-
-      expect(status).toBe(204);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, asset1.id);
-      expect(asset.stack).not.toBeUndefined();
-      expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset3.id })]));
-    });
-
-    it('should remove all stack children', async () => {
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ removeParent: true, ids: [asset2.id, asset3.id] });
-
-      expect(status).toBe(204);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, asset1.id);
-      expect(asset.stack).toBeUndefined();
-    });
-
-    it('should merge stack children', async () => {
-      const newParent = await createAsset(user1, new Date('1970-01-01'));
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: newParent.id, ids: [asset1.id] });
-
-      expect(status).toBe(204);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, newParent.id);
-      expect(asset.stack).not.toBeUndefined();
-      expect(asset.stack).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({ id: asset1.id }),
-          expect.objectContaining({ id: asset2.id }),
-          expect.objectContaining({ id: asset3.id }),
-        ]),
-      );
-    });
-  });
-
-  describe('PUT /asset/stack/parent', () => {
-    beforeEach(async () => {
-      const { status } = await request(server)
-        .put('/asset')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ stackParentId: asset1.id, ids: [asset2.id, asset3.id] });
-
-      expect(status).toBe(204);
-    });
-
-    it('should require authentication', async () => {
-      const { status, body } = await request(server).put('/asset/stack/parent');
-
-      expect(status).toBe(401);
-      expect(body).toEqual(errorStub.unauthorized);
-    });
-
-    it('should require a valid id', async () => {
-      const { status, body } = await request(server)
-        .put('/asset/stack/parent')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ oldParentId: uuidStub.invalid, newParentId: uuidStub.invalid });
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.badRequest());
-    });
-
-    it('should require access', async () => {
-      const { status, body } = await request(server)
-        .put('/asset/stack/parent')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ oldParentId: asset4.id, newParentId: asset1.id });
-
-      expect(status).toBe(400);
-      expect(body).toEqual(errorStub.noPermission);
-    });
-
-    it('should make old parent child of new parent', async () => {
-      const { status } = await request(server)
-        .put('/asset/stack/parent')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ oldParentId: asset1.id, newParentId: asset2.id });
-
-      expect(status).toBe(200);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, asset2.id);
-      expect(asset.stack).not.toBeUndefined();
-      expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset1.id })]));
-    });
-
-    it('should make all childrens of old parent, a child of new parent', async () => {
-      const { status } = await request(server)
-        .put('/asset/stack/parent')
-        .set('Authorization', `Bearer ${user1.accessToken}`)
-        .send({ oldParentId: asset1.id, newParentId: asset2.id });
-
-      expect(status).toBe(200);
-
-      const asset = await api.assetApi.get(server, user1.accessToken, asset2.id);
-      expect(asset.stack).not.toBeUndefined();
-      expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset3.id })]));
-    });
-  });
-
-  const getAssetIdsWithoutFaces = async () => {
-    const assetPagination = usePagination(10, (pagination) =>
-      assetRepository.getWithout(pagination, WithoutProperty.FACES),
-    );
-    let assets: AssetEntity[] = [];
-    for await (const assetsPage of assetPagination) {
-      assets = [...assets, ...assetsPage];
-    }
-    return assets.map((a) => a.id);
-  };
-
-  describe(AssetRepository.name, () => {
-    describe('getWithout', () => {
-      describe('WithoutProperty.FACES', () => {
-        beforeEach(async () => {
-          await assetRepository.save({ id: asset1.id, resizePath: '/path/to/resize' });
-          expect(await getAssetIdsWithoutFaces()).toContain(asset1.id);
-        });
-
-        describe('with recognized faces', () => {
-          beforeEach(async () => {
-            const personRepository = app.get<IPersonRepository>(IPersonRepository);
-            const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
-            await personRepository.createFaces([
-              {
-                assetId: asset1.id,
-                personId: person.id,
-                embedding: Array.from({ length: 512 }, Math.random),
-              },
-            ]);
-          });
-
-          it('should not return asset with facesRecognizedAt unset', async () => {
-            expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id);
-          });
-
-          it('should not return asset with facesRecognizedAt set', async () => {
-            await assetRepository.upsertJobStatus({ assetId: asset1.id, facesRecognizedAt: new Date() });
-            expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id);
-          });
-        });
-
-        describe('without recognized faces', () => {
-          it('should return asset with facesRecognizedAt unset', async () => {
-            expect(await getAssetIdsWithoutFaces()).toContain(asset1.id);
-          });
-
-          it('should not return asset with facesRecognizedAt set', async () => {
-            expect(await getAssetIdsWithoutFaces()).toContain(asset1.id);
-            await assetRepository.upsertJobStatus({ assetId: asset1.id, facesRecognizedAt: new Date() });
-            expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id);
-          });
-        });
-      });
-    });
-  });
-});
diff --git a/server/e2e/api/utils.ts b/server/e2e/api/utils.ts
deleted file mode 100644
index c03c4ada55..0000000000
--- a/server/e2e/api/utils.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { AssetCreate, IJobRepository, IMetadataRepository, LibraryResponseDto } from '@app/domain';
-import { AppModule } from '@app/immich';
-import { InfraModule, InfraTestModule, dataSource } from '@app/infra';
-import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
-import { INestApplication } from '@nestjs/common';
-import { Test } from '@nestjs/testing';
-import { DateTime } from 'luxon';
-import { randomBytes } from 'node:crypto';
-import { EntityTarget, ObjectLiteral } from 'typeorm';
-import { AppService } from '../../src/microservices/app.service';
-import { newJobRepositoryMock, newMetadataRepositoryMock } from '../../test';
-
-export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 });
-export const yesterday = today.minus({ days: 1 });
-
-export interface ResetOptions {
-  entities?: EntityTarget<ObjectLiteral>[];
-}
-export const db = {
-  reset: async (options?: ResetOptions) => {
-    if (!dataSource.isInitialized) {
-      await dataSource.initialize();
-    }
-    await dataSource.transaction(async (em) => {
-      const entities = options?.entities || [];
-      const tableNames =
-        entities.length > 0
-          ? entities.map((entity) => em.getRepository(entity).metadata.tableName)
-          : dataSource.entityMetadatas
-              .map((entity) => entity.tableName)
-              .filter((tableName) => !tableName.startsWith('geodata'));
-
-      if (tableNames.includes('asset_stack')) {
-        await em.query(`DELETE FROM "asset_stack" CASCADE;`);
-      }
-      let deleteUsers = false;
-      for (const tableName of tableNames) {
-        if (tableName === 'users') {
-          deleteUsers = true;
-          continue;
-        }
-        await em.query(`DELETE FROM ${tableName} CASCADE;`);
-      }
-      if (deleteUsers) {
-        await em.query(`DELETE FROM "users" CASCADE;`);
-      }
-    });
-  },
-  disconnect: async () => {
-    if (dataSource.isInitialized) {
-      await dataSource.destroy();
-    }
-  },
-};
-
-let app: INestApplication;
-
-export const testApp = {
-  create: async (): Promise<INestApplication> => {
-    const moduleFixture = await Test.createTestingModule({ imports: [AppModule], providers: [AppService] })
-      .overrideModule(InfraModule)
-      .useModule(InfraTestModule)
-      .overrideProvider(IJobRepository)
-      .useValue(newJobRepositoryMock())
-      .overrideProvider(IMetadataRepository)
-      .useValue(newMetadataRepositoryMock())
-      .compile();
-
-    app = await moduleFixture.createNestApplication().init();
-    await app.get(AppService).init();
-
-    return app;
-  },
-  reset: async (options?: ResetOptions) => {
-    await db.reset(options);
-  },
-  teardown: async () => {
-    if (app) {
-      await app.get(AppService).teardown();
-      await app.close();
-    }
-    await db.disconnect();
-  },
-};
-
-function randomDate(start: Date, end: Date): Date {
-  return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
-}
-
-let assetCount = 0;
-export function generateAsset(
-  userId: string,
-  libraries: LibraryResponseDto[],
-  other: Partial<AssetEntity> = {},
-): AssetCreate {
-  const id = assetCount++;
-  const { fileCreatedAt = randomDate(new Date(1970, 1, 1), new Date(2023, 1, 1)) } = other;
-
-  return {
-    createdAt: today.toJSDate(),
-    updatedAt: today.toJSDate(),
-    ownerId: userId,
-    checksum: randomBytes(20),
-    originalPath: `/tests/test_${id}`,
-    deviceAssetId: `test_${id}`,
-    deviceId: 'e2e-test',
-    libraryId: (
-      libraries.find(({ ownerId, type }) => ownerId === userId && type === LibraryType.UPLOAD) as LibraryResponseDto
-    ).id,
-    isVisible: true,
-    fileCreatedAt,
-    fileModifiedAt: new Date(),
-    localDateTime: fileCreatedAt,
-    type: AssetType.IMAGE,
-    originalFileName: `test_${id}`,
-    ...other,
-  };
-}
diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts
index 8d2a1b79bc..63d4395866 100644
--- a/server/e2e/client/asset-api.ts
+++ b/server/e2e/client/asset-api.ts
@@ -1,77 +1,10 @@
 import { AssetResponseDto } from '@app/domain';
-import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
-import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
-import { randomBytes } from 'node:crypto';
 import request from 'supertest';
 
-type UploadDto = Partial<CreateAssetDto> & { content?: Buffer; filename?: string };
-
-const asset = {
-  deviceAssetId: 'test-1',
-  deviceId: 'test',
-  fileCreatedAt: new Date(),
-  fileModifiedAt: new Date(),
-};
-
 export const assetApi = {
-  create: async (
-    server: any,
-    accessToken: string,
-    dto?: Omit<CreateAssetDto, 'assetData'>,
-  ): Promise<AssetResponseDto> => {
-    dto = dto || asset;
-    const { status, body } = await request(server)
-      .post(`/asset/upload`)
-      .field('deviceAssetId', dto.deviceAssetId)
-      .field('deviceId', dto.deviceId)
-      .field('fileCreatedAt', dto.fileCreatedAt.toISOString())
-      .field('fileModifiedAt', dto.fileModifiedAt.toISOString())
-      .attach('assetData', randomBytes(32), 'example.jpg')
-      .set('Authorization', `Bearer ${accessToken}`);
-
-    expect([200, 201].includes(status)).toBe(true);
-
-    return body as AssetResponseDto;
-  },
-  get: async (server: any, accessToken: string, id: string): Promise<AssetResponseDto> => {
-    const { body, status } = await request(server).get(`/asset/${id}`).set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(200);
-    return body as AssetResponseDto;
-  },
   getAllAssets: async (server: any, accessToken: string) => {
     const { body, status } = await request(server).get(`/asset/`).set('Authorization', `Bearer ${accessToken}`);
     expect(status).toBe(200);
     return body as AssetResponseDto[];
   },
-  upload: async (server: any, accessToken: string, deviceAssetId: string, dto: UploadDto = {}) => {
-    const { content, filename, isFavorite = false, isArchived = false } = dto;
-    const { body, status } = await request(server)
-      .post('/asset/upload')
-      .set('Authorization', `Bearer ${accessToken}`)
-      .field('deviceAssetId', deviceAssetId)
-      .field('deviceId', 'TEST')
-      .field('fileCreatedAt', new Date().toISOString())
-      .field('fileModifiedAt', new Date().toISOString())
-      .field('isFavorite', isFavorite)
-      .field('isArchived', isArchived)
-      .field('duration', '0:00:00.000000')
-      .attach('assetData', content || randomBytes(32), filename || 'example.jpg');
-
-    expect(status).toBe(201);
-    return body as AssetFileUploadResponseDto;
-  },
-  getWebpThumbnail: async (server: any, accessToken: string, assetId: string) => {
-    const { body, status } = await request(server)
-      .get(`/asset/thumbnail/${assetId}`)
-      .set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(200);
-    return body;
-  },
-  getJpegThumbnail: async (server: any, accessToken: string, assetId: string) => {
-    const { body, status } = await request(server)
-      .get(`/asset/thumbnail/${assetId}?format=JPEG`)
-      .set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(200);
-    return body;
-  },
 };
diff --git a/server/e2e/client/auth-api.ts b/server/e2e/client/auth-api.ts
index f0206d3376..e89e6d0576 100644
--- a/server/e2e/client/auth-api.ts
+++ b/server/e2e/client/auth-api.ts
@@ -1,4 +1,4 @@
-import { LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain';
+import { LoginResponseDto, UserResponseDto } from '@app/domain';
 import { adminSignupStub, loginResponseStub, loginStub } from '@test';
 import request from 'supertest';
 
@@ -17,14 +17,6 @@ export const authApi = {
     expect(body).toMatchObject({ accessToken: expect.any(String) });
     expect(status).toBe(201);
 
-    return body as LoginResponseDto;
-  },
-  login: async (server: any, dto: LoginCredentialDto) => {
-    const { status, body } = await request(server).post('/auth/login').send(dto);
-
-    expect(status).toEqual(201);
-    expect(body).toMatchObject({ accessToken: expect.any(String) });
-
     return body as LoginResponseDto;
   },
 };
diff --git a/server/e2e/client/index.ts b/server/e2e/client/index.ts
index b0464a34d8..b4aa2a141b 100644
--- a/server/e2e/client/index.ts
+++ b/server/e2e/client/index.ts
@@ -1,15 +1,9 @@
 import { assetApi } from './asset-api';
 import { authApi } from './auth-api';
 import { libraryApi } from './library-api';
-import { sharedLinkApi } from './shared-link-api';
-import { trashApi } from './trash-api';
-import { userApi } from './user-api';
 
 export const api = {
   authApi,
   assetApi,
   libraryApi,
-  sharedLinkApi,
-  trashApi,
-  userApi,
 };
diff --git a/server/e2e/client/library-api.ts b/server/e2e/client/library-api.ts
index e0b1331267..070683eb01 100644
--- a/server/e2e/client/library-api.ts
+++ b/server/e2e/client/library-api.ts
@@ -1,12 +1,4 @@
-import {
-  CreateLibraryDto,
-  LibraryResponseDto,
-  LibraryStatsResponseDto,
-  ScanLibraryDto,
-  UpdateLibraryDto,
-  ValidateLibraryDto,
-  ValidateLibraryResponseDto,
-} from '@app/domain';
+import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from '@app/domain';
 import request from 'supertest';
 
 export const libraryApi = {
@@ -38,34 +30,4 @@ export const libraryApi = {
       .send(dto);
     expect(status).toBe(204);
   },
-  removeOfflineFiles: async (server: any, accessToken: string, id: string) => {
-    const { status } = await request(server)
-      .post(`/library/${id}/removeOffline`)
-      .set('Authorization', `Bearer ${accessToken}`)
-      .send();
-    expect(status).toBe(204);
-  },
-  getLibraryStatistics: async (server: any, accessToken: string, id: string): Promise<LibraryStatsResponseDto> => {
-    const { body, status } = await request(server)
-      .get(`/library/${id}/statistics`)
-      .set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(200);
-    return body;
-  },
-  update: async (server: any, accessToken: string, id: string, data: UpdateLibraryDto) => {
-    const { body, status } = await request(server)
-      .put(`/library/${id}`)
-      .set('Authorization', `Bearer ${accessToken}`)
-      .send(data);
-    expect(status).toBe(200);
-    return body as LibraryResponseDto;
-  },
-  validate: async (server: any, accessToken: string, id: string, data: ValidateLibraryDto) => {
-    const { body, status } = await request(server)
-      .post(`/library/${id}/validate`)
-      .set('Authorization', `Bearer ${accessToken}`)
-      .send(data);
-    expect(status).toBe(200);
-    return body as ValidateLibraryResponseDto;
-  },
 };
diff --git a/server/e2e/client/shared-link-api.ts b/server/e2e/client/shared-link-api.ts
deleted file mode 100644
index c34093b0ac..0000000000
--- a/server/e2e/client/shared-link-api.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { SharedLinkCreateDto, SharedLinkResponseDto } from '@app/domain';
-import request from 'supertest';
-
-export const sharedLinkApi = {
-  create: async (server: any, accessToken: string, dto: SharedLinkCreateDto) => {
-    const { status, body } = await request(server)
-      .post('/shared-link')
-      .set('Authorization', `Bearer ${accessToken}`)
-      .send(dto);
-    expect(status).toBe(201);
-    return body as SharedLinkResponseDto;
-  },
-};
diff --git a/server/e2e/client/trash-api.ts b/server/e2e/client/trash-api.ts
deleted file mode 100644
index a381253f50..0000000000
--- a/server/e2e/client/trash-api.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import request from 'supertest';
-import type { App } from 'supertest/types';
-
-export const trashApi = {
-  async empty(server: App, accessToken: string) {
-    const { status } = await request(server).post('/trash/empty').set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(204);
-  },
-  async restore(server: App, accessToken: string) {
-    const { status } = await request(server).post('/trash/restore').set('Authorization', `Bearer ${accessToken}`);
-    expect(status).toBe(204);
-  },
-};
diff --git a/server/e2e/client/user-api.ts b/server/e2e/client/user-api.ts
deleted file mode 100644
index c538db3a8f..0000000000
--- a/server/e2e/client/user-api.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { CreateUserDto, UpdateUserDto, UserResponseDto } from '@app/domain';
-import request from 'supertest';
-
-export const userApi = {
-  create: async (server: any, accessToken: string, dto: CreateUserDto) => {
-    const { status, body } = await request(server)
-      .post('/user')
-      .set('Authorization', `Bearer ${accessToken}`)
-      .send(dto);
-
-    expect(status).toBe(201);
-    expect(body).toMatchObject({
-      id: expect.any(String),
-      createdAt: expect.any(String),
-      updatedAt: expect.any(String),
-      email: dto.email,
-    });
-
-    return body as UserResponseDto;
-  },
-  update: async (server: any, accessToken: string, dto: UpdateUserDto) => {
-    const { status, body } = await request(server).put('/user').set('Authorization', `Bearer ${accessToken}`).send(dto);
-
-    expect(status).toBe(200);
-    expect(body).toMatchObject({ id: dto.id });
-
-    return body as UserResponseDto;
-  },
-  delete: async (server: any, accessToken: string, id: string) => {
-    const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`);
-
-    expect(status).toBe(200);
-    expect(body).toMatchObject({ id, deletedAt: expect.any(String) });
-
-    return body as UserResponseDto;
-  },
-};
diff --git a/server/package.json b/server/package.json
index 98ee13c1b1..1e3d31c7d8 100644
--- a/server/package.json
+++ b/server/package.json
@@ -23,7 +23,6 @@
     "test:cov": "jest --coverage",
     "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
     "e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand",
-    "e2e:api": "jest --config e2e/api/jest-e2e.json --runInBand",
     "typeorm": "typeorm",
     "typeorm:migrations:create": "typeorm migration:create",
     "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",