diff --git a/docker/.env.test b/docker/.env.test
index d48b4f53fa..23f58fe805 100644
--- a/docker/.env.test
+++ b/docker/.env.test
@@ -10,9 +10,6 @@ REDIS_HOSTNAME=immich-redis-test
 # Upload File Config
 UPLOAD_LOCATION=./upload
 
-# JWT SECRET
-JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
-
 # MAPBOX
 ## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
 ENABLE_MAPBOX=false
diff --git a/docker/example.env b/docker/example.env
index 922a873c05..2cfb1e7351 100644
--- a/docker/example.env
+++ b/docker/example.env
@@ -30,16 +30,6 @@ REDIS_HOSTNAME=immich_redis
 
 UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
 
-###################################################################################
-# JWT SECRET
-#
-# This JWT_SECRET is used to sign the authentication keys for user login
-# You should set it to a long randomly generated value
-# You can use this command to generate one: openssl rand -base64 128
-###################################################################################
-
-JWT_SECRET=
-
 ###################################################################################
 # Reverse Geocoding
 #
diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md
index c48eb32bd6..362c32627a 100644
--- a/docs/docs/developer/setup.md
+++ b/docs/docs/developer/setup.md
@@ -24,7 +24,7 @@ All the services are packaged to run as with single Docker Compose command.
 
 1. Clone the project repo.
 2. Run `cp docker/example.env docker/.env`.
-3. Edit `docker/.env` to provide values for the required variables `UPLOAD_LOCATION` and `JWT_SECRET`.
+3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
 4. From the root directory, run:
 
 ```bash title="Start development server"
diff --git a/docs/docs/install/docker-compose.md b/docs/docs/install/docker-compose.md
index a062233069..1241cec421 100644
--- a/docs/docs/install/docker-compose.md
+++ b/docs/docs/install/docker-compose.md
@@ -63,15 +63,6 @@ UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_ba
 
 LOG_LEVEL=simple
 
-###################################################################################
-# JWT SECRET
-###################################################################################
-
-# This JWT_SECRET is used to sign the authentication keys for user login
-# You should set it to a long randomly generated value
-# You can use this command to generate one: openssl rand -base64 128
-JWT_SECRET=
-
 ###################################################################################
 # Reverse Geocoding
 ####################################################################################
@@ -102,11 +93,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
 
 - Populate custom database information if necessary.
 - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
-- Populate a secret value for `JWT_SECRET`. You can use the command below to generate a secure key:
-
-```bash title="Command to generate secure JWT_SECRET key"
-openssl rand -base64 128
-```
 
 ### Step 3 - Start the containers
 
diff --git a/docs/docs/install/portainer.md b/docs/docs/install/portainer.md
index de3767e780..09c5917f3d 100644
--- a/docs/docs/install/portainer.md
+++ b/docs/docs/install/portainer.md
@@ -40,11 +40,6 @@ Install Immich using Portainer's Stack feature.
 
 * Populate custom database information if necessary.
 * Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
-* Populate a secret value for `JWT_SECRET`. You can use the command below to generate a secure key:
-
-```bash title="Generate secure JWT_SECRET key"
-openssl rand -base64 128
-```
 
 11. Click on "**Deploy the stack**".
 
diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md
index 235a47b4de..3cf5f80fa8 100644
--- a/docs/docs/install/unraid.md
+++ b/docs/docs/install/unraid.md
@@ -55,7 +55,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
 6.  Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
 7.  Past the entire contents of the [Immich example.env](https://raw.githubusercontent.com/immich-app/immich/main/docker/example.env) file into the Unraid editor, then **before saving** edit the following:
 
-    - `JWT_SECRET`: Generate a unique secret and paste the value here > Can be generated by either typing `openssl rand -base64 128` in your terminal or copying from [uuidgenerator](https://www.uuidgenerator.net/version1)
     - `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
 
       <img
diff --git a/install.sh b/install.sh
index 4ef5fda537..e3a1b89a6e 100755
--- a/install.sh
+++ b/install.sh
@@ -45,12 +45,6 @@ populate_upload_location() {
   replace_env_value "UPLOAD_LOCATION" $upload_location
 }
 
-generate_jwt_secret() {
-  echo "Generating JWT_SECRET value..."
-  jwt_secret=$(openssl rand -base64 128)
-  replace_env_value "JWT_SECRET" $jwt_secret
-}
-
 start_docker_compose() {
   echo "Starting Immich's docker containers"
 
@@ -92,5 +86,4 @@ create_immich_directory
 download_docker_compose_file
 download_dot_env_file
 populate_upload_location
-generate_jwt_secret
 start_docker_compose
diff --git a/server/apps/immich/src/api-v1/communication/communication.gateway.ts b/server/apps/immich/src/api-v1/communication/communication.gateway.ts
index 9ca2a3e23f..11eccab01d 100644
--- a/server/apps/immich/src/api-v1/communication/communication.gateway.ts
+++ b/server/apps/immich/src/api-v1/communication/communication.gateway.ts
@@ -19,8 +19,7 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
   async handleConnection(client: Socket) {
     try {
       this.logger.log(`New websocket connection: ${client.id}`);
-
-      const user = await this.authService.validateSocket(client);
+      const user = await this.authService.validate(client.request.headers);
       if (user) {
         client.join(user.id);
       } else {
@@ -28,7 +27,8 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
         client.disconnect();
       }
     } catch (e) {
-      // Logger.error(`Error establish websocket conneciton ${e}`, 'HandleWebscoketConnection');
+      client.emit('error', 'unauthorized');
+      client.disconnect();
     }
   }
 }
diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts
index d6100f6342..1001f80d82 100644
--- a/server/apps/immich/src/app.module.ts
+++ b/server/apps/immich/src/app.module.ts
@@ -1,7 +1,6 @@
 import { immichAppConfig } from '@app/common/config';
 import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
 import { AssetModule } from './api-v1/asset/asset.module';
-import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
 import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
 import { ConfigModule } from '@nestjs/config';
 import { ServerInfoModule } from './api-v1/server-info/server-info.module';
@@ -23,6 +22,9 @@ import {
   SystemConfigController,
   UserController,
 } from './controllers';
+import { PublicShareStrategy } from './modules/immich-auth/strategies/public-share.strategy';
+import { APIKeyStrategy } from './modules/immich-auth/strategies/api-key.strategy';
+import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.strategy';
 
 @Module({
   imports: [
@@ -34,8 +36,6 @@ import {
 
     AssetModule,
 
-    ImmichJwtModule,
-
     DeviceInfoModule,
 
     ServerInfoModule,
@@ -64,7 +64,7 @@ import {
     SystemConfigController,
     UserController,
   ],
-  providers: [],
+  providers: [UserAuthStrategy, APIKeyStrategy, PublicShareStrategy],
 })
 export class AppModule implements NestModule {
   // TODO: check if consumer is needed or remove
diff --git a/server/apps/immich/src/decorators/authenticated.decorator.ts b/server/apps/immich/src/decorators/authenticated.decorator.ts
index 4939ec5f20..6e3009e2f5 100644
--- a/server/apps/immich/src/decorators/authenticated.decorator.ts
+++ b/server/apps/immich/src/decorators/authenticated.decorator.ts
@@ -1,7 +1,7 @@
 import { UseGuards } from '@nestjs/common';
 import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware';
 import { RouteNotSharedGuard } from '../middlewares/route-not-shared-guard.middleware';
-import { AuthGuard } from '../modules/immich-jwt/guards/auth.guard';
+import { AuthGuard } from '../modules/immich-auth/guards/auth.guard';
 
 interface AuthenticatedOptions {
   admin?: boolean;
diff --git a/server/apps/immich/src/global.d.ts b/server/apps/immich/src/global.d.ts
index a5867b0ed6..ff1f5fa03c 100644
--- a/server/apps/immich/src/global.d.ts
+++ b/server/apps/immich/src/global.d.ts
@@ -4,5 +4,8 @@ declare global {
   namespace Express {
     // eslint-disable-next-line @typescript-eslint/no-empty-interface
     interface User extends AuthUserDto {}
+    export interface Request {
+      user: AuthUserDto;
+    }
   }
 }
diff --git a/server/apps/immich/src/main.ts b/server/apps/immich/src/main.ts
index d1d3ed1d0b..37dd5ecfad 100644
--- a/server/apps/immich/src/main.ts
+++ b/server/apps/immich/src/main.ts
@@ -40,9 +40,6 @@ async function bootstrap() {
     .addBearerAuth({
       type: 'http',
       scheme: 'Bearer',
-      bearerFormat: 'JWT',
-      name: 'JWT',
-      description: 'Enter JWT token',
       in: 'header',
     })
     .addServer('/api')
diff --git a/server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts b/server/apps/immich/src/modules/immich-auth/guards/auth.guard.ts
similarity index 72%
rename from server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts
rename to server/apps/immich/src/modules/immich-auth/guards/auth.guard.ts
index 6bb237725d..9babdf8175 100644
--- a/server/apps/immich/src/modules/immich-jwt/guards/auth.guard.ts
+++ b/server/apps/immich/src/modules/immich-auth/guards/auth.guard.ts
@@ -1,8 +1,8 @@
 import { Injectable } from '@nestjs/common';
 import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
 import { API_KEY_STRATEGY } from '../strategies/api-key.strategy';
-import { JWT_STRATEGY } from '../strategies/jwt.strategy';
+import { AUTH_COOKIE_STRATEGY } from '../strategies/user-auth.strategy';
 import { PUBLIC_SHARE_STRATEGY } from '../strategies/public-share.strategy';
 
 @Injectable()
-export class AuthGuard extends PassportAuthGuard([PUBLIC_SHARE_STRATEGY, JWT_STRATEGY, API_KEY_STRATEGY]) {}
+export class AuthGuard extends PassportAuthGuard([PUBLIC_SHARE_STRATEGY, AUTH_COOKIE_STRATEGY, API_KEY_STRATEGY]) {}
diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts b/server/apps/immich/src/modules/immich-auth/strategies/api-key.strategy.ts
similarity index 100%
rename from server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts
rename to server/apps/immich/src/modules/immich-auth/strategies/api-key.strategy.ts
diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts b/server/apps/immich/src/modules/immich-auth/strategies/public-share.strategy.ts
similarity index 100%
rename from server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts
rename to server/apps/immich/src/modules/immich-auth/strategies/public-share.strategy.ts
diff --git a/server/apps/immich/src/modules/immich-auth/strategies/user-auth.strategy.ts b/server/apps/immich/src/modules/immich-auth/strategies/user-auth.strategy.ts
new file mode 100644
index 0000000000..3ce1d9c670
--- /dev/null
+++ b/server/apps/immich/src/modules/immich-auth/strategies/user-auth.strategy.ts
@@ -0,0 +1,24 @@
+import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { PassportStrategy } from '@nestjs/passport';
+import { AuthService, AuthUserDto, UserService } from '@app/domain';
+import { Strategy } from 'passport-custom';
+import { Request } from 'express';
+
+export const AUTH_COOKIE_STRATEGY = 'auth-cookie';
+
+@Injectable()
+export class UserAuthStrategy extends PassportStrategy(Strategy, AUTH_COOKIE_STRATEGY) {
+  constructor(private userService: UserService, private authService: AuthService) {
+    super();
+  }
+
+  async validate(request: Request): Promise<AuthUserDto> {
+    const authUser = await this.authService.validate(request.headers);
+
+    if (!authUser) {
+      throw new UnauthorizedException('Incorrect token provided');
+    }
+
+    return authUser;
+  }
+}
diff --git a/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts b/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts
deleted file mode 100644
index e3922d5fc1..0000000000
--- a/server/apps/immich/src/modules/immich-jwt/immich-jwt.module.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Module } from '@nestjs/common';
-import { APIKeyStrategy } from './strategies/api-key.strategy';
-import { JwtStrategy } from './strategies/jwt.strategy';
-import { PublicShareStrategy } from './strategies/public-share.strategy';
-
-@Module({
-  providers: [JwtStrategy, APIKeyStrategy, PublicShareStrategy],
-})
-export class ImmichJwtModule {}
diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts
deleted file mode 100644
index 1468dbfec9..0000000000
--- a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { AuthService, AuthUserDto, JwtPayloadDto, jwtSecret } from '@app/domain';
-import { Injectable } from '@nestjs/common';
-import { PassportStrategy } from '@nestjs/passport';
-import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt';
-
-export const JWT_STRATEGY = 'jwt';
-
-@Injectable()
-export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) {
-  constructor(private authService: AuthService) {
-    super({
-      jwtFromRequest: ExtractJwt.fromExtractors([
-        (req) => authService.extractJwtFromCookie(req.cookies),
-        (req) => authService.extractJwtFromHeader(req.headers),
-      ]),
-      ignoreExpiration: false,
-      secretOrKey: jwtSecret,
-    } as StrategyOptions);
-  }
-
-  async validate(payload: JwtPayloadDto): Promise<AuthUserDto> {
-    return this.authService.validatePayload(payload);
-  }
-}
diff --git a/server/apps/immich/test/album.e2e-spec.ts b/server/apps/immich/test/album.e2e-spec.ts
index e9de296efe..17a2bd23aa 100644
--- a/server/apps/immich/test/album.e2e-spec.ts
+++ b/server/apps/immich/test/album.e2e-spec.ts
@@ -5,10 +5,10 @@ import { clearDb, getAuthUser, authCustom } from './test-utils';
 import { InfraModule } from '@app/infra';
 import { AlbumModule } from '../src/api-v1/album/album.module';
 import { CreateAlbumDto } from '../src/api-v1/album/dto/create-album.dto';
-import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
 import { AuthUserDto } from '../src/decorators/auth-user.decorator';
 import { AuthService, DomainModule, UserService } from '@app/domain';
 import { DataSource } from 'typeorm';
+import { AppModule } from '../src/app.module';
 
 function _createAlbum(app: INestApplication, data: CreateAlbumDto) {
   return request(app.getHttpServer()).post('/album').send(data);
@@ -21,7 +21,7 @@ describe('Album', () => {
   describe('without auth', () => {
     beforeAll(async () => {
       const moduleFixture: TestingModule = await Test.createTestingModule({
-        imports: [DomainModule.register({ imports: [InfraModule] }), AlbumModule, ImmichJwtModule],
+        imports: [DomainModule.register({ imports: [InfraModule] }), AppModule],
       }).compile();
 
       app = moduleFixture.createNestApplication();
diff --git a/server/apps/immich/test/jest-e2e.json b/server/apps/immich/test/jest-e2e.json
index c0014051c5..6867cf956e 100644
--- a/server/apps/immich/test/jest-e2e.json
+++ b/server/apps/immich/test/jest-e2e.json
@@ -1,5 +1,6 @@
 {
   "moduleFileExtensions": ["js", "json", "ts"],
+  "modulePaths": ["<rootDir>", "<rootDir>../../../"],
   "rootDir": ".",
   "testEnvironment": "node",
   "testRegex": ".e2e-spec.ts$",
diff --git a/server/apps/immich/test/test-utils.ts b/server/apps/immich/test/test-utils.ts
index 9ab27ff3c9..67de60d19f 100644
--- a/server/apps/immich/test/test-utils.ts
+++ b/server/apps/immich/test/test-utils.ts
@@ -2,7 +2,7 @@ import { CanActivate, ExecutionContext } from '@nestjs/common';
 import { TestingModuleBuilder } from '@nestjs/testing';
 import { DataSource } from 'typeorm';
 import { AuthUserDto } from '../src/decorators/auth-user.decorator';
-import { AuthGuard } from '../src/modules/immich-jwt/guards/auth.guard';
+import { AuthGuard } from '../src/modules/immich-auth/guards/auth.guard';
 
 type CustomAuthCallback = () => AuthUserDto;
 
diff --git a/server/apps/immich/test/user.e2e-spec.ts b/server/apps/immich/test/user.e2e-spec.ts
index dde23b141a..173f2e357d 100644
--- a/server/apps/immich/test/user.e2e-spec.ts
+++ b/server/apps/immich/test/user.e2e-spec.ts
@@ -3,11 +3,11 @@ import { INestApplication } from '@nestjs/common';
 import request from 'supertest';
 import { clearDb, authCustom } from './test-utils';
 import { InfraModule } from '@app/infra';
-import { ImmichJwtModule } from '../src/modules/immich-jwt/immich-jwt.module';
 import { DomainModule, CreateUserDto, UserService, AuthUserDto } from '@app/domain';
 import { DataSource } from 'typeorm';
 import { UserController } from '../src/controllers';
 import { AuthService } from '@app/domain';
+import { AppModule } from '../src/app.module';
 
 function _createUser(userService: UserService, data: CreateUserDto) {
   return userService.createUser(data);
@@ -25,7 +25,7 @@ describe('User', () => {
   describe('without auth', () => {
     beforeAll(async () => {
       const moduleFixture: TestingModule = await Test.createTestingModule({
-        imports: [DomainModule.register({ imports: [InfraModule] }), ImmichJwtModule],
+        imports: [DomainModule.register({ imports: [InfraModule] }), AppModule],
         controllers: [UserController],
       }).compile();
 
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index 1641c9bf8b..dca5d80492 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -2722,8 +2722,6 @@
         "scheme": "Bearer",
         "bearerFormat": "JWT",
         "type": "http",
-        "name": "JWT",
-        "description": "Enter JWT token",
         "in": "header"
       }
     },
diff --git a/server/libs/common/src/config/app.config.ts b/server/libs/common/src/config/app.config.ts
index 71a47b0b18..db619b32da 100644
--- a/server/libs/common/src/config/app.config.ts
+++ b/server/libs/common/src/config/app.config.ts
@@ -1,20 +1,5 @@
-import { Logger } from '@nestjs/common';
 import { ConfigModuleOptions } from '@nestjs/config';
 import Joi from 'joi';
-import { createSecretKey, generateKeySync } from 'node:crypto';
-
-const jwtSecretValidator: Joi.CustomValidator<string> = (value) => {
-  const key = createSecretKey(value, 'base64');
-  const keySizeBits = (key.symmetricKeySize ?? 0) * 8;
-
-  if (keySizeBits < 128) {
-    const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64');
-    Logger.warn('The current JWT_SECRET key is insecure. It should be at least 128 bits long!');
-    Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`);
-  }
-
-  return value;
-};
 
 const WHEN_DB_URL_SET = Joi.when('DB_URL', {
   is: Joi.exist(),
@@ -31,7 +16,6 @@ export const immichAppConfig: ConfigModuleOptions = {
     DB_PASSWORD: WHEN_DB_URL_SET,
     DB_DATABASE_NAME: WHEN_DB_URL_SET,
     DB_URL: Joi.string().optional(),
-    JWT_SECRET: Joi.string().required().custom(jwtSecretValidator),
     DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
     REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
     LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose', 'debug', 'log', 'warn', 'error').default('log'),
diff --git a/server/libs/domain/src/api-key/api-key.repository.ts b/server/libs/domain/src/api-key/api-key.repository.ts
index 961d521648..76182fe1da 100644
--- a/server/libs/domain/src/api-key/api-key.repository.ts
+++ b/server/libs/domain/src/api-key/api-key.repository.ts
@@ -10,7 +10,7 @@ export interface IKeyRepository {
    * Includes the hashed `key` for verification
    * @param id
    */
-  getKey(id: number): Promise<APIKeyEntity | null>;
+  getKey(hashedToken: string): Promise<APIKeyEntity | null>;
   getById(userId: string, id: number): Promise<APIKeyEntity | null>;
   getByUserId(userId: string): Promise<APIKeyEntity[]>;
 }
diff --git a/server/libs/domain/src/api-key/api-key.service.spec.ts b/server/libs/domain/src/api-key/api-key.service.spec.ts
index 0b9516af74..4761734c3a 100644
--- a/server/libs/domain/src/api-key/api-key.service.spec.ts
+++ b/server/libs/domain/src/api-key/api-key.service.spec.ts
@@ -1,6 +1,6 @@
 import { APIKeyEntity } from '@app/infra/db/entities';
 import { BadRequestException, UnauthorizedException } from '@nestjs/common';
-import { authStub, entityStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test';
+import { authStub, userEntityStub, newCryptoRepositoryMock, newKeyRepositoryMock } from '../../test';
 import { ICryptoRepository } from '../auth';
 import { IKeyRepository } from './api-key.repository';
 import { APIKeyService } from './api-key.service';
@@ -10,10 +10,10 @@ const adminKey = Object.freeze({
   name: 'My Key',
   key: 'my-api-key (hashed)',
   userId: authStub.admin.id,
-  user: entityStub.admin,
+  user: userEntityStub.admin,
 } as APIKeyEntity);
 
-const token = Buffer.from('1:my-api-key', 'utf8').toString('base64');
+const token = Buffer.from('my-api-key', 'utf8').toString('base64');
 
 describe(APIKeyService.name, () => {
   let sut: APIKeyService;
@@ -38,7 +38,7 @@ describe(APIKeyService.name, () => {
         userId: authStub.admin.id,
       });
       expect(cryptoMock.randomBytes).toHaveBeenCalled();
-      expect(cryptoMock.hash).toHaveBeenCalled();
+      expect(cryptoMock.hashSha256).toHaveBeenCalled();
     });
 
     it('should not require a name', async () => {
@@ -52,7 +52,7 @@ describe(APIKeyService.name, () => {
         userId: authStub.admin.id,
       });
       expect(cryptoMock.randomBytes).toHaveBeenCalled();
-      expect(cryptoMock.hash).toHaveBeenCalled();
+      expect(cryptoMock.hashSha256).toHaveBeenCalled();
     });
   });
 
@@ -126,8 +126,7 @@ describe(APIKeyService.name, () => {
 
       await expect(sut.validate(token)).rejects.toBeInstanceOf(UnauthorizedException);
 
-      expect(keyMock.getKey).toHaveBeenCalledWith(1);
-      expect(cryptoMock.compareSync).not.toHaveBeenCalled();
+      expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)');
     });
 
     it('should validate the token', async () => {
@@ -135,8 +134,7 @@ describe(APIKeyService.name, () => {
 
       await expect(sut.validate(token)).resolves.toEqual(authStub.admin);
 
-      expect(keyMock.getKey).toHaveBeenCalledWith(1);
-      expect(cryptoMock.compareSync).toHaveBeenCalledWith('my-api-key', 'my-api-key (hashed)');
+      expect(keyMock.getKey).toHaveBeenCalledWith('bXktYXBpLWtleQ== (hashed)');
     });
   });
 });
diff --git a/server/libs/domain/src/api-key/api-key.service.ts b/server/libs/domain/src/api-key/api-key.service.ts
index aefff8f64d..c5bf096933 100644
--- a/server/libs/domain/src/api-key/api-key.service.ts
+++ b/server/libs/domain/src/api-key/api-key.service.ts
@@ -1,4 +1,3 @@
-import { UserEntity } from '@app/infra/db/entities';
 import { BadRequestException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
 import { AuthUserDto, ICryptoRepository } from '../auth';
 import { IKeyRepository } from './api-key.repository';
@@ -14,15 +13,13 @@ export class APIKeyService {
   ) {}
 
   async create(authUser: AuthUserDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
-    const key = this.crypto.randomBytes(24).toString('base64').replace(/\W/g, '');
+    const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
     const entity = await this.repository.create({
-      key: await this.crypto.hash(key, 10),
+      key: this.crypto.hashSha256(secret),
       name: dto.name || 'API Key',
       userId: authUser.id,
     });
 
-    const secret = Buffer.from(`${entity.id}:${key}`, 'utf8').toString('base64');
-
     return { secret, apiKey: mapKey(entity) };
   }
 
@@ -60,22 +57,18 @@ export class APIKeyService {
   }
 
   async validate(token: string): Promise<AuthUserDto> {
-    const [_id, key] = Buffer.from(token, 'base64').toString('utf8').split(':');
-    const id = Number(_id);
+    const hashedToken = this.crypto.hashSha256(token);
+    const keyEntity = await this.repository.getKey(hashedToken);
+    if (keyEntity?.user) {
+      const user = keyEntity.user;
 
-    if (id && key) {
-      const entity = await this.repository.getKey(id);
-      if (entity?.user && entity?.key && this.crypto.compareSync(key, entity.key)) {
-        const user = entity.user as UserEntity;
-
-        return {
-          id: user.id,
-          email: user.email,
-          isAdmin: user.isAdmin,
-          isPublicUser: false,
-          isAllowUpload: true,
-        };
-      }
+      return {
+        id: user.id,
+        email: user.email,
+        isAdmin: user.isAdmin,
+        isPublicUser: false,
+        isAllowUpload: true,
+      };
     }
 
     throw new UnauthorizedException('Invalid API Key');
diff --git a/server/libs/domain/src/auth/auth.config.ts b/server/libs/domain/src/auth/auth.config.ts
deleted file mode 100644
index 71dcd3a98c..0000000000
--- a/server/libs/domain/src/auth/auth.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { JwtModuleOptions } from '@nestjs/jwt';
-import { jwtSecret } from './auth.constant';
-
-export const jwtConfig: JwtModuleOptions = {
-  secret: jwtSecret,
-  signOptions: { expiresIn: '30d' },
-};
diff --git a/server/libs/domain/src/auth/auth.constant.ts b/server/libs/domain/src/auth/auth.constant.ts
index fbab227755..2bf04f6721 100644
--- a/server/libs/domain/src/auth/auth.constant.ts
+++ b/server/libs/domain/src/auth/auth.constant.ts
@@ -1,4 +1,3 @@
-export const jwtSecret = process.env.JWT_SECRET;
 export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
 export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
 export enum AuthType {
diff --git a/server/libs/domain/src/auth/auth.core.ts b/server/libs/domain/src/auth/auth.core.ts
index 109fac8dab..7cf7ac8e0a 100644
--- a/server/libs/domain/src/auth/auth.core.ts
+++ b/server/libs/domain/src/auth/auth.core.ts
@@ -4,8 +4,9 @@ import { ISystemConfigRepository } from '../system-config';
 import { SystemConfigCore } from '../system-config/system-config.core';
 import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
 import { ICryptoRepository } from './crypto.repository';
-import { JwtPayloadDto } from './dto/jwt-payload.dto';
 import { LoginResponseDto, mapLoginResponse } from './response-dto';
+import { IUserTokenRepository, UserTokenCore } from '@app/domain';
+import cookieParser from 'cookie';
 
 export type JwtValidationResult = {
   status: boolean;
@@ -13,11 +14,14 @@ export type JwtValidationResult = {
 };
 
 export class AuthCore {
+  private userTokenCore: UserTokenCore;
   constructor(
     private cryptoRepository: ICryptoRepository,
     configRepository: ISystemConfigRepository,
+    userTokenRepository: IUserTokenRepository,
     private config: SystemConfig,
   ) {
+    this.userTokenCore = new UserTokenCore(cryptoRepository, userTokenRepository);
     const configCore = new SystemConfigCore(configRepository);
     configCore.config$.subscribe((config) => (this.config = config));
   }
@@ -33,8 +37,8 @@ export class AuthCore {
     let accessTokenCookie = '';
 
     if (isSecure) {
-      accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
-      authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
+      accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
+      authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
     } else {
       accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
       authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
@@ -42,9 +46,8 @@ export class AuthCore {
     return [accessTokenCookie, authTypeCookie];
   }
 
-  public createLoginResponse(user: UserEntity, authType: AuthType, isSecure: boolean) {
-    const payload: JwtPayloadDto = { userId: user.id, email: user.email };
-    const accessToken = this.generateToken(payload);
+  public async createLoginResponse(user: UserEntity, authType: AuthType, isSecure: boolean) {
+    const accessToken = await this.userTokenCore.createToken(user);
     const response = mapLoginResponse(user, accessToken);
     const cookie = this.getCookies(response, authType, isSecure);
     return { response, cookie };
@@ -54,12 +57,12 @@ export class AuthCore {
     if (!user || !user.password) {
       return false;
     }
-    return this.cryptoRepository.compareSync(inputPassword, user.password);
+    return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
   }
 
-  extractJwtFromHeader(headers: IncomingHttpHeaders) {
+  extractTokenFromHeader(headers: IncomingHttpHeaders) {
     if (!headers.authorization) {
-      return null;
+      return this.extractTokenFromCookie(cookieParser.parse(headers.cookie || ''));
     }
 
     const [type, accessToken] = headers.authorization.split(' ');
@@ -70,11 +73,7 @@ export class AuthCore {
     return accessToken;
   }
 
-  extractJwtFromCookie(cookies: Record<string, string>) {
+  extractTokenFromCookie(cookies: Record<string, string>) {
     return cookies?.[IMMICH_ACCESS_COOKIE] || null;
   }
-
-  private generateToken(payload: JwtPayloadDto) {
-    return this.cryptoRepository.signJwt({ ...payload });
-  }
 }
diff --git a/server/libs/domain/src/auth/auth.service.spec.ts b/server/libs/domain/src/auth/auth.service.spec.ts
index db71cb54b9..486d26d82a 100644
--- a/server/libs/domain/src/auth/auth.service.spec.ts
+++ b/server/libs/domain/src/auth/auth.service.spec.ts
@@ -3,13 +3,13 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 import { generators, Issuer } from 'openid-client';
 import { Socket } from 'socket.io';
 import {
-  authStub,
-  entityStub,
+  userEntityStub,
   loginResponseStub,
   newCryptoRepositoryMock,
   newSystemConfigRepositoryMock,
   newUserRepositoryMock,
   systemConfigStub,
+  userTokenEntityStub,
 } from '../../test';
 import { ISystemConfigRepository } from '../system-config';
 import { IUserRepository } from '../user';
@@ -17,6 +17,9 @@ import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.
 import { AuthService } from './auth.service';
 import { ICryptoRepository } from './crypto.repository';
 import { SignUpDto } from './dto';
+import { IUserTokenRepository } from '@app/domain';
+import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock';
+import { IncomingHttpHeaders } from 'http';
 
 const email = 'test@immich.com';
 const sub = 'my-auth-user-sub';
@@ -47,6 +50,7 @@ describe('AuthService', () => {
   let cryptoMock: jest.Mocked<ICryptoRepository>;
   let userMock: jest.Mocked<IUserRepository>;
   let configMock: jest.Mocked<ISystemConfigRepository>;
+  let userTokenMock: jest.Mocked<IUserTokenRepository>;
   let callbackMock: jest.Mock;
   let create: (config: SystemConfig) => AuthService;
 
@@ -76,8 +80,9 @@ describe('AuthService', () => {
     cryptoMock = newCryptoRepositoryMock();
     userMock = newUserRepositoryMock();
     configMock = newSystemConfigRepositoryMock();
+    userTokenMock = newUserTokenRepositoryMock();
 
-    create = (config) => new AuthService(cryptoMock, configMock, userMock, config);
+    create = (config) => new AuthService(cryptoMock, configMock, userMock, userTokenMock, config);
 
     sut = create(systemConfigStub.enabled);
   });
@@ -106,13 +111,15 @@ describe('AuthService', () => {
     });
 
     it('should successfully log the user in', async () => {
-      userMock.getByEmail.mockResolvedValue(entityStub.user1);
+      userMock.getByEmail.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
       await expect(sut.login(fixtures.login, CLIENT_IP, true)).resolves.toEqual(loginResponseStub.user1password);
       expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
     });
 
     it('should generate the cookie headers (insecure)', async () => {
-      userMock.getByEmail.mockResolvedValue(entityStub.user1);
+      userMock.getByEmail.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
       await expect(sut.login(fixtures.login, CLIENT_IP, false)).resolves.toEqual(loginResponseStub.user1insecure);
       expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
     });
@@ -131,7 +138,7 @@ describe('AuthService', () => {
       await sut.changePassword(authUser, dto);
 
       expect(userMock.getByEmail).toHaveBeenCalledWith(authUser.email, true);
-      expect(cryptoMock.compareSync).toHaveBeenCalledWith('old-password', 'hash-password');
+      expect(cryptoMock.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
     });
 
     it('should throw when auth user email is not found', async () => {
@@ -147,7 +154,7 @@ describe('AuthService', () => {
       const authUser = { email: 'test@imimch.com' } as UserEntity;
       const dto = { password: 'old-password', newPassword: 'new-password' };
 
-      cryptoMock.compareSync.mockReturnValue(false);
+      cryptoMock.compareBcrypt.mockReturnValue(false);
 
       userMock.getByEmail.mockResolvedValue({
         email: 'test@immich.com',
@@ -161,8 +168,6 @@ describe('AuthService', () => {
       const authUser = { email: 'test@imimch.com' } as UserEntity;
       const dto = { password: 'old-password', newPassword: 'new-password' };
 
-      cryptoMock.compareSync.mockReturnValue(false);
-
       userMock.getByEmail.mockResolvedValue({
         email: 'test@immich.com',
         password: '',
@@ -212,52 +217,64 @@ describe('AuthService', () => {
     });
   });
 
-  describe('validateSocket', () => {
+  describe('validate - socket connections', () => {
     it('should validate using authorization header', async () => {
-      userMock.get.mockResolvedValue(entityStub.user1);
-      const client = { handshake: { headers: { authorization: 'Bearer jwt-token' } } };
-      await expect(sut.validateSocket(client as Socket)).resolves.toEqual(entityStub.user1);
+      userMock.get.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
+      const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
+      await expect(sut.validate((client as Socket).request.headers)).resolves.toEqual(userEntityStub.user1);
     });
   });
 
-  describe('validatePayload', () => {
+  describe('validate - api request', () => {
     it('should throw if no user is found', async () => {
       userMock.get.mockResolvedValue(null);
-      await expect(sut.validatePayload({ email: 'a', userId: 'test' })).rejects.toBeInstanceOf(UnauthorizedException);
+      await expect(sut.validate({ email: 'a', userId: 'test' })).rejects.toBeInstanceOf(UnauthorizedException);
     });
 
     it('should return an auth dto', async () => {
-      userMock.get.mockResolvedValue(entityStub.admin);
-      await expect(sut.validatePayload({ email: 'a', userId: 'test' })).resolves.toEqual(authStub.admin);
+      userMock.get.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.get.mockResolvedValue(userTokenEntityStub.userToken);
+      await expect(
+        sut.validate({ cookie: 'immich_access_token=auth_token', email: 'a', userId: 'test' }),
+      ).resolves.toEqual(userEntityStub.user1);
     });
   });
 
-  describe('extractJwtFromCookie', () => {
+  describe('extractTokenFromHeader - Cookie', () => {
     it('should extract the access token', () => {
-      const cookie = { [IMMICH_ACCESS_COOKIE]: 'signed-jwt', [IMMICH_AUTH_TYPE_COOKIE]: 'password' };
-      expect(sut.extractJwtFromCookie(cookie)).toEqual('signed-jwt');
+      const cookie: IncomingHttpHeaders = {
+        cookie: `${IMMICH_ACCESS_COOKIE}=signed-jwt;${IMMICH_AUTH_TYPE_COOKIE}=password`,
+      };
+      expect(sut.extractTokenFromHeader(cookie)).toEqual('signed-jwt');
     });
 
     it('should work with no cookies', () => {
-      expect(sut.extractJwtFromCookie(undefined as any)).toBeNull();
+      const cookie: IncomingHttpHeaders = {
+        cookie: undefined,
+      };
+      expect(sut.extractTokenFromHeader(cookie)).toBeNull();
     });
 
     it('should work on empty cookies', () => {
-      expect(sut.extractJwtFromCookie({})).toBeNull();
+      const cookie: IncomingHttpHeaders = {
+        cookie: '',
+      };
+      expect(sut.extractTokenFromHeader(cookie)).toBeNull();
     });
   });
 
-  describe('extractJwtFromHeader', () => {
+  describe('extractTokenFromHeader - Bearer Auth', () => {
     it('should extract the access token', () => {
-      expect(sut.extractJwtFromHeader({ authorization: `Bearer signed-jwt` })).toEqual('signed-jwt');
+      expect(sut.extractTokenFromHeader({ authorization: `Bearer signed-jwt` })).toEqual('signed-jwt');
     });
 
     it('should work without the auth header', () => {
-      expect(sut.extractJwtFromHeader({})).toBeNull();
+      expect(sut.extractTokenFromHeader({})).toBeNull();
     });
 
     it('should ignore basic auth', () => {
-      expect(sut.extractJwtFromHeader({ authorization: `Basic stuff` })).toBeNull();
+      expect(sut.extractTokenFromHeader({ authorization: `Basic stuff` })).toBeNull();
     });
   });
 });
diff --git a/server/libs/domain/src/auth/auth.service.ts b/server/libs/domain/src/auth/auth.service.ts
index 0fcb799672..6872cbcecd 100644
--- a/server/libs/domain/src/auth/auth.service.ts
+++ b/server/libs/domain/src/auth/auth.service.ts
@@ -7,20 +7,20 @@ import {
   Logger,
   UnauthorizedException,
 } from '@nestjs/common';
-import * as cookieParser from 'cookie';
 import { IncomingHttpHeaders } from 'http';
-import { Socket } from 'socket.io';
 import { OAuthCore } from '../oauth/oauth.core';
 import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
-import { IUserRepository, UserCore, UserResponseDto } from '../user';
-import { AuthType, jwtSecret } from './auth.constant';
+import { IUserRepository, UserCore } from '../user';
+import { AuthType } from './auth.constant';
 import { AuthCore } from './auth.core';
 import { ICryptoRepository } from './crypto.repository';
-import { AuthUserDto, ChangePasswordDto, JwtPayloadDto, LoginCredentialDto, SignUpDto } from './dto';
+import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, SignUpDto } from './dto';
 import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
+import { IUserTokenRepository, UserTokenCore } from '@app/domain/user-token';
 
 @Injectable()
 export class AuthService {
+  private userTokenCore: UserTokenCore;
   private authCore: AuthCore;
   private oauthCore: OAuthCore;
   private userCore: UserCore;
@@ -31,11 +31,14 @@ export class AuthService {
     @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
     @Inject(IUserRepository) userRepository: IUserRepository,
-    @Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig,
+    @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
+    @Inject(INITIAL_SYSTEM_CONFIG)
+    initialConfig: SystemConfig,
   ) {
-    this.authCore = new AuthCore(cryptoRepository, configRepository, initialConfig);
+    this.userTokenCore = new UserTokenCore(cryptoRepository, userTokenRepository);
+    this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
     this.oauthCore = new OAuthCore(configRepository, initialConfig);
-    this.userCore = new UserCore(userRepository);
+    this.userCore = new UserCore(userRepository, cryptoRepository);
   }
 
   public async login(
@@ -49,7 +52,7 @@ export class AuthService {
 
     let user = await this.userCore.getByEmail(loginCredential.email, true);
     if (user) {
-      const isAuthenticated = await this.authCore.validatePassword(loginCredential.password, user);
+      const isAuthenticated = this.authCore.validatePassword(loginCredential.password, user);
       if (!isAuthenticated) {
         user = null;
       }
@@ -81,7 +84,7 @@ export class AuthService {
       throw new UnauthorizedException();
     }
 
-    const valid = await this.authCore.validatePassword(password, user);
+    const valid = this.authCore.validatePassword(password, user);
     if (!valid) {
       throw new BadRequestException('Wrong password');
     }
@@ -112,49 +115,28 @@ export class AuthService {
     }
   }
 
-  async validateSocket(client: Socket): Promise<UserResponseDto | null> {
-    try {
-      const headers = client.handshake.headers;
-      const accessToken =
-        this.extractJwtFromCookie(cookieParser.parse(headers.cookie || '')) || this.extractJwtFromHeader(headers);
-
-      if (accessToken) {
-        const payload = await this.cryptoRepository.verifyJwtAsync<JwtPayloadDto>(accessToken, { secret: jwtSecret });
-        if (payload?.userId && payload?.email) {
-          const user = await this.userCore.get(payload.userId);
-          if (user) {
-            return user;
-          }
-        }
-      }
-    } catch (e) {
-      return null;
-    }
-    return null;
-  }
-
-  async validatePayload(payload: JwtPayloadDto) {
-    const { userId } = payload;
-    const user = await this.userCore.get(userId);
-    if (!user) {
-      throw new UnauthorizedException('Failure to validate JWT payload');
+  public async validate(headers: IncomingHttpHeaders): Promise<AuthUserDto> {
+    const tokenValue = this.extractTokenFromHeader(headers);
+    if (!tokenValue) {
+      throw new UnauthorizedException('No access token provided in request');
     }
 
-    const authUser = new AuthUserDto();
-    authUser.id = user.id;
-    authUser.email = user.email;
-    authUser.isAdmin = user.isAdmin;
-    authUser.isPublicUser = false;
-    authUser.isAllowUpload = true;
+    const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
+    const user = await this.userTokenCore.getUserByToken(hashedToken);
+    if (user) {
+      return {
+        ...user,
+        isPublicUser: false,
+        isAllowUpload: true,
+        isAllowDownload: true,
+        isShowExif: true,
+      };
+    }
 
-    return authUser;
+    throw new UnauthorizedException('Invalid access token provided');
   }
 
-  extractJwtFromCookie(cookies: Record<string, string>) {
-    return this.authCore.extractJwtFromCookie(cookies);
-  }
-
-  extractJwtFromHeader(headers: IncomingHttpHeaders) {
-    return this.authCore.extractJwtFromHeader(headers);
+  extractTokenFromHeader(headers: IncomingHttpHeaders) {
+    return this.authCore.extractTokenFromHeader(headers);
   }
 }
diff --git a/server/libs/domain/src/auth/crypto.repository.ts b/server/libs/domain/src/auth/crypto.repository.ts
index e12240c74d..d400b017da 100644
--- a/server/libs/domain/src/auth/crypto.repository.ts
+++ b/server/libs/domain/src/auth/crypto.repository.ts
@@ -1,11 +1,8 @@
-import { JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
-
 export const ICryptoRepository = 'ICryptoRepository';
 
 export interface ICryptoRepository {
   randomBytes(size: number): Buffer;
-  hash(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
-  compareSync(data: Buffer | string, encrypted: string): boolean;
-  signJwt(payload: string | Buffer | object, options?: JwtSignOptions): string;
-  verifyJwtAsync<T extends object = any>(token: string, options?: JwtVerifyOptions): Promise<T>;
+  hashSha256(data: string): string;
+  hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
+  compareBcrypt(data: string | Buffer, encrypted: string): boolean;
 }
diff --git a/server/libs/domain/src/auth/index.ts b/server/libs/domain/src/auth/index.ts
index be6def62bb..118de239ea 100644
--- a/server/libs/domain/src/auth/index.ts
+++ b/server/libs/domain/src/auth/index.ts
@@ -1,4 +1,3 @@
-export * from './auth.config';
 export * from './auth.constant';
 export * from './auth.service';
 export * from './crypto.repository';
diff --git a/server/libs/domain/src/domain.module.ts b/server/libs/domain/src/domain.module.ts
index 8079851276..23b4adf190 100644
--- a/server/libs/domain/src/domain.module.ts
+++ b/server/libs/domain/src/domain.module.ts
@@ -13,7 +13,6 @@ const providers: Provider[] = [
   SystemConfigService,
   UserService,
   ShareService,
-
   {
     provide: INITIAL_SYSTEM_CONFIG,
     inject: [SystemConfigService],
diff --git a/server/libs/domain/src/index.ts b/server/libs/domain/src/index.ts
index 809f8b0618..38b491751d 100644
--- a/server/libs/domain/src/index.ts
+++ b/server/libs/domain/src/index.ts
@@ -9,3 +9,4 @@ export * from './share';
 export * from './system-config';
 export * from './tag';
 export * from './user';
+export * from './user-token';
diff --git a/server/libs/domain/src/oauth/oauth.service.spec.ts b/server/libs/domain/src/oauth/oauth.service.spec.ts
index 5408ee0392..0cf18587d1 100644
--- a/server/libs/domain/src/oauth/oauth.service.spec.ts
+++ b/server/libs/domain/src/oauth/oauth.service.spec.ts
@@ -3,17 +3,20 @@ import { BadRequestException } from '@nestjs/common';
 import { generators, Issuer } from 'openid-client';
 import {
   authStub,
-  entityStub,
+  userEntityStub,
   loginResponseStub,
   newCryptoRepositoryMock,
   newSystemConfigRepositoryMock,
   newUserRepositoryMock,
   systemConfigStub,
+  userTokenEntityStub,
 } from '../../test';
 import { ICryptoRepository } from '../auth';
 import { OAuthService } from '../oauth';
 import { ISystemConfigRepository } from '../system-config';
 import { IUserRepository } from '../user';
+import { IUserTokenRepository } from '@app/domain';
+import { newUserTokenRepositoryMock } from '../../test/user-token.repository.mock';
 
 const email = 'user@immich.com';
 const sub = 'my-auth-user-sub';
@@ -35,6 +38,7 @@ describe('OAuthService', () => {
   let userMock: jest.Mocked<IUserRepository>;
   let cryptoMock: jest.Mocked<ICryptoRepository>;
   let configMock: jest.Mocked<ISystemConfigRepository>;
+  let userTokenMock: jest.Mocked<IUserTokenRepository>;
   let callbackMock: jest.Mock;
   let create: (config: SystemConfig) => OAuthService;
 
@@ -60,8 +64,9 @@ describe('OAuthService', () => {
     cryptoMock = newCryptoRepositoryMock();
     configMock = newSystemConfigRepositoryMock();
     userMock = newUserRepositoryMock();
+    userTokenMock = newUserTokenRepositoryMock();
 
-    create = (config) => new OAuthService(cryptoMock, configMock, userMock, config);
+    create = (config) => new OAuthService(cryptoMock, configMock, userMock, userTokenMock, config);
 
     sut = create(systemConfigStub.disabled);
   });
@@ -106,23 +111,25 @@ describe('OAuthService', () => {
 
     it('should link an existing user', async () => {
       sut = create(systemConfigStub.noAutoRegister);
-      userMock.getByEmail.mockResolvedValue(entityStub.user1);
-      userMock.update.mockResolvedValue(entityStub.user1);
+      userMock.getByEmail.mockResolvedValue(userEntityStub.user1);
+      userMock.update.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
 
       await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, true)).resolves.toEqual(
         loginResponseStub.user1oauth,
       );
 
       expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
-      expect(userMock.update).toHaveBeenCalledWith(entityStub.user1.id, { oauthId: sub });
+      expect(userMock.update).toHaveBeenCalledWith(userEntityStub.user1.id, { oauthId: sub });
     });
 
     it('should allow auto registering by default', async () => {
       sut = create(systemConfigStub.enabled);
 
       userMock.getByEmail.mockResolvedValue(null);
-      userMock.getAdmin.mockResolvedValue(entityStub.user1);
-      userMock.create.mockResolvedValue(entityStub.user1);
+      userMock.getAdmin.mockResolvedValue(userEntityStub.user1);
+      userMock.create.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
 
       await expect(sut.login({ url: 'http://immich/auth/login?code=abc123' }, true)).resolves.toEqual(
         loginResponseStub.user1oauth,
@@ -135,7 +142,8 @@ describe('OAuthService', () => {
     it('should use the mobile redirect override', async () => {
       sut = create(systemConfigStub.override);
 
-      userMock.getByOAuthId.mockResolvedValue(entityStub.user1);
+      userMock.getByOAuthId.mockResolvedValue(userEntityStub.user1);
+      userTokenMock.create.mockResolvedValue(userTokenEntityStub.userToken);
 
       await sut.login({ url: `app.immich:/?code=abc123` }, true);
 
@@ -147,7 +155,7 @@ describe('OAuthService', () => {
     it('should link an account', async () => {
       sut = create(systemConfigStub.enabled);
 
-      userMock.update.mockResolvedValue(entityStub.user1);
+      userMock.update.mockResolvedValue(userEntityStub.user1);
 
       await sut.link(authStub.user1, { url: 'http://immich/user-settings?code=abc123' });
 
@@ -171,7 +179,7 @@ describe('OAuthService', () => {
     it('should unlink an account', async () => {
       sut = create(systemConfigStub.enabled);
 
-      userMock.update.mockResolvedValue(entityStub.user1);
+      userMock.update.mockResolvedValue(userEntityStub.user1);
 
       await sut.unlink(authStub.user1);
 
diff --git a/server/libs/domain/src/oauth/oauth.service.ts b/server/libs/domain/src/oauth/oauth.service.ts
index f054f019e8..7d919d75ad 100644
--- a/server/libs/domain/src/oauth/oauth.service.ts
+++ b/server/libs/domain/src/oauth/oauth.service.ts
@@ -7,6 +7,7 @@ import { IUserRepository, UserCore, UserResponseDto } from '../user';
 import { OAuthCallbackDto, OAuthConfigDto } from './dto';
 import { OAuthCore } from './oauth.core';
 import { OAuthConfigResponseDto } from './response-dto';
+import { IUserTokenRepository } from '@app/domain/user-token';
 
 @Injectable()
 export class OAuthService {
@@ -20,10 +21,11 @@ export class OAuthService {
     @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
     @Inject(IUserRepository) userRepository: IUserRepository,
+    @Inject(IUserTokenRepository) userTokenRepository: IUserTokenRepository,
     @Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig,
   ) {
-    this.authCore = new AuthCore(cryptoRepository, configRepository, initialConfig);
-    this.userCore = new UserCore(userRepository);
+    this.authCore = new AuthCore(cryptoRepository, configRepository, userTokenRepository, initialConfig);
+    this.userCore = new UserCore(userRepository, cryptoRepository);
     this.oauthCore = new OAuthCore(configRepository, initialConfig);
   }
 
diff --git a/server/libs/domain/src/share/share.service.spec.ts b/server/libs/domain/src/share/share.service.spec.ts
index 9f997b4a56..c8dd994fc3 100644
--- a/server/libs/domain/src/share/share.service.spec.ts
+++ b/server/libs/domain/src/share/share.service.spec.ts
@@ -1,7 +1,7 @@
 import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
 import {
   authStub,
-  entityStub,
+  userEntityStub,
   newCryptoRepositoryMock,
   newSharedLinkRepositoryMock,
   newUserRepositoryMock,
@@ -50,7 +50,7 @@ describe(ShareService.name, () => {
 
     it('should accept a valid key', async () => {
       shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
-      userMock.get.mockResolvedValue(entityStub.admin);
+      userMock.get.mockResolvedValue(userEntityStub.admin);
       await expect(sut.validate('key')).resolves.toEqual(authStub.adminSharedLink);
     });
   });
diff --git a/server/libs/domain/src/share/share.service.ts b/server/libs/domain/src/share/share.service.ts
index eca46d97ab..e175b6e943 100644
--- a/server/libs/domain/src/share/share.service.ts
+++ b/server/libs/domain/src/share/share.service.ts
@@ -25,7 +25,7 @@ export class ShareService {
     @Inject(IUserRepository) userRepository: IUserRepository,
   ) {
     this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
-    this.userCore = new UserCore(userRepository);
+    this.userCore = new UserCore(userRepository, cryptoRepository);
   }
 
   async validate(key: string): Promise<AuthUserDto> {
diff --git a/server/libs/domain/src/user-token/index.ts b/server/libs/domain/src/user-token/index.ts
new file mode 100644
index 0000000000..46c83640c7
--- /dev/null
+++ b/server/libs/domain/src/user-token/index.ts
@@ -0,0 +1,2 @@
+export * from './user-token.repository';
+export * from './user-token.core';
diff --git a/server/libs/domain/src/user-token/user-token.core.ts b/server/libs/domain/src/user-token/user-token.core.ts
new file mode 100644
index 0000000000..7ae3b27835
--- /dev/null
+++ b/server/libs/domain/src/user-token/user-token.core.ts
@@ -0,0 +1,28 @@
+import { UserEntity } from '@app/infra/db/entities';
+import { Injectable } from '@nestjs/common';
+import { ICryptoRepository } from '../auth';
+import { IUserTokenRepository } from './user-token.repository';
+
+@Injectable()
+export class UserTokenCore {
+  constructor(private crypto: ICryptoRepository, private repository: IUserTokenRepository) {}
+
+  public async getUserByToken(tokenValue: string): Promise<UserEntity | null> {
+    const token = await this.repository.get(tokenValue);
+    if (token?.user) {
+      return token.user;
+    }
+    return null;
+  }
+
+  public async createToken(user: UserEntity): Promise<string> {
+    const key = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
+    const token = this.crypto.hashSha256(key);
+    await this.repository.create({
+      token,
+      user,
+    });
+
+    return key;
+  }
+}
diff --git a/server/libs/domain/src/user-token/user-token.repository.ts b/server/libs/domain/src/user-token/user-token.repository.ts
new file mode 100644
index 0000000000..a084d22e8f
--- /dev/null
+++ b/server/libs/domain/src/user-token/user-token.repository.ts
@@ -0,0 +1,9 @@
+import { UserTokenEntity } from '@app/infra/db/entities';
+
+export const IUserTokenRepository = 'IUserTokenRepository';
+
+export interface IUserTokenRepository {
+  create(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
+  delete(userToken: string): Promise<void>;
+  get(userToken: string): Promise<UserTokenEntity | null>;
+}
diff --git a/server/libs/domain/src/user/user.core.ts b/server/libs/domain/src/user/user.core.ts
index a1cc54f42b..30edc160bf 100644
--- a/server/libs/domain/src/user/user.core.ts
+++ b/server/libs/domain/src/user/user.core.ts
@@ -10,14 +10,14 @@ import {
 import { hash } from 'bcrypt';
 import { constants, createReadStream, ReadStream } from 'fs';
 import fs from 'fs/promises';
-import { AuthUserDto } from '../auth';
+import { AuthUserDto, ICryptoRepository } from '../auth';
 import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto';
 import { IUserRepository, UserListFilter } from './user.repository';
 
 const SALT_ROUNDS = 10;
 
 export class UserCore {
-  constructor(private userRepository: IUserRepository) {}
+  constructor(private userRepository: IUserRepository, private cryptoRepository: ICryptoRepository) {}
 
   async updateUser(authUser: AuthUserDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
     if (!(authUser.isAdmin || authUser.id === id)) {
@@ -37,7 +37,7 @@ export class UserCore {
 
     try {
       if (dto.password) {
-        dto.password = await hash(dto.password, SALT_ROUNDS);
+        dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
       }
 
       return this.userRepository.update(id, dto);
diff --git a/server/libs/domain/src/user/user.service.spec.ts b/server/libs/domain/src/user/user.service.spec.ts
index deb61f16fd..62df6b8ce5 100644
--- a/server/libs/domain/src/user/user.service.spec.ts
+++ b/server/libs/domain/src/user/user.service.spec.ts
@@ -2,8 +2,8 @@ import { IUserRepository } from './user.repository';
 import { UserEntity } from '@app/infra/db/entities';
 import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
 import { when } from 'jest-when';
-import { newUserRepositoryMock } from '../../test';
-import { AuthUserDto } from '../auth';
+import { newCryptoRepositoryMock, newUserRepositoryMock } from '../../test';
+import { AuthUserDto, ICryptoRepository } from '../auth';
 import { UpdateUserDto } from './dto/update-user.dto';
 import { UserService } from './user.service';
 
@@ -77,10 +77,12 @@ const adminUserResponse = Object.freeze({
 describe(UserService.name, () => {
   let sut: UserService;
   let userRepositoryMock: jest.Mocked<IUserRepository>;
+  let cryptoRepositoryMock: jest.Mocked<ICryptoRepository>;
 
   beforeEach(async () => {
     userRepositoryMock = newUserRepositoryMock();
-    sut = new UserService(userRepositoryMock);
+    cryptoRepositoryMock = newCryptoRepositoryMock();
+    sut = new UserService(userRepositoryMock, cryptoRepositoryMock);
 
     when(userRepositoryMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
     when(userRepositoryMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
diff --git a/server/libs/domain/src/user/user.service.ts b/server/libs/domain/src/user/user.service.ts
index e0d02876b9..74e669fcef 100644
--- a/server/libs/domain/src/user/user.service.ts
+++ b/server/libs/domain/src/user/user.service.ts
@@ -1,7 +1,7 @@
 import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
 import { randomBytes } from 'crypto';
 import { ReadStream } from 'fs';
-import { AuthUserDto } from '../auth';
+import { AuthUserDto, ICryptoRepository } from '../auth';
 import { IUserRepository } from '../user';
 import { CreateUserDto } from './dto/create-user.dto';
 import { UpdateUserDto } from './dto/update-user.dto';
@@ -17,8 +17,11 @@ import { UserCore } from './user.core';
 @Injectable()
 export class UserService {
   private userCore: UserCore;
-  constructor(@Inject(IUserRepository) userRepository: IUserRepository) {
-    this.userCore = new UserCore(userRepository);
+  constructor(
+    @Inject(IUserRepository) userRepository: IUserRepository,
+    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
+  ) {
+    this.userCore = new UserCore(userRepository, cryptoRepository);
   }
 
   async getAllUsers(authUser: AuthUserDto, isAll: boolean): Promise<UserResponseDto[]> {
diff --git a/server/libs/domain/test/crypto.repository.mock.ts b/server/libs/domain/test/crypto.repository.mock.ts
index fe7e1dccc9..1e37222e44 100644
--- a/server/libs/domain/test/crypto.repository.mock.ts
+++ b/server/libs/domain/test/crypto.repository.mock.ts
@@ -3,9 +3,8 @@ import { ICryptoRepository } from '../src';
 export const newCryptoRepositoryMock = (): jest.Mocked<ICryptoRepository> => {
   return {
     randomBytes: jest.fn().mockReturnValue(Buffer.from('random-bytes', 'utf8')),
-    compareSync: jest.fn().mockReturnValue(true),
-    hash: jest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)),
-    signJwt: jest.fn().mockReturnValue('signed-jwt'),
-    verifyJwtAsync: jest.fn().mockResolvedValue({ userId: 'test', email: 'test' }),
+    compareBcrypt: jest.fn().mockReturnValue(true),
+    hashBcrypt: jest.fn().mockImplementation((input) => Promise.resolve(`${input} (hashed)`)),
+    hashSha256: jest.fn().mockImplementation((input) => `${input} (hashed)`),
   };
 };
diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts
index 3f267f4274..510c7e7893 100644
--- a/server/libs/domain/test/fixtures.ts
+++ b/server/libs/domain/test/fixtures.ts
@@ -1,4 +1,11 @@
-import { AssetType, SharedLinkEntity, SharedLinkType, SystemConfig, UserEntity } from '@app/infra/db/entities';
+import {
+  AssetType,
+  SharedLinkEntity,
+  SharedLinkType,
+  SystemConfig,
+  UserEntity,
+  UserTokenEntity,
+} from '@app/infra/db/entities';
 import { AlbumResponseDto, AssetResponseDto, AuthUserDto, ExifResponseDto, SharedLinkResponseDto } from '../src';
 
 const today = new Date();
@@ -81,6 +88,8 @@ export const authStub = {
     isAdmin: false,
     isPublicUser: false,
     isAllowUpload: true,
+    isAllowDownload: true,
+    isShowExif: true,
   }),
   adminSharedLink: Object.freeze<AuthUserDto>({
     id: 'admin_id',
@@ -104,7 +113,7 @@ export const authStub = {
   }),
 };
 
-export const entityStub = {
+export const userEntityStub = {
   admin: Object.freeze<UserEntity>({
     ...authStub.admin,
     password: 'admin_password',
@@ -129,6 +138,16 @@ export const entityStub = {
   }),
 };
 
+export const userTokenEntityStub = {
+  userToken: Object.freeze<UserTokenEntity>({
+    id: 'token-id',
+    token: 'auth_token',
+    user: userEntityStub.user1,
+    createdAt: '2021-01-01',
+    updatedAt: '2021-01-01',
+  }),
+};
+
 export const systemConfigStub = {
   defaults: Object.freeze({
     ffmpeg: {
@@ -204,7 +223,7 @@ export const systemConfigStub = {
 export const loginResponseStub = {
   user1oauth: {
     response: {
-      accessToken: 'signed-jwt',
+      accessToken: 'cmFuZG9tLWJ5dGVz',
       userId: 'immich_id',
       userEmail: 'immich@test.com',
       firstName: 'immich_first_name',
@@ -214,13 +233,13 @@ export const loginResponseStub = {
       shouldChangePassword: false,
     },
     cookie: [
-      'immich_access_token=signed-jwt; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
-      'immich_auth_type=oauth; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
+      'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
+      'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
     ],
   },
   user1password: {
     response: {
-      accessToken: 'signed-jwt',
+      accessToken: 'cmFuZG9tLWJ5dGVz',
       userId: 'immich_id',
       userEmail: 'immich@test.com',
       firstName: 'immich_first_name',
@@ -230,13 +249,13 @@ export const loginResponseStub = {
       shouldChangePassword: false,
     },
     cookie: [
-      'immich_access_token=signed-jwt; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
-      'immich_auth_type=password; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
+      'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
+      'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=604800; SameSite=Strict;',
     ],
   },
   user1insecure: {
     response: {
-      accessToken: 'signed-jwt',
+      accessToken: 'cmFuZG9tLWJ5dGVz',
       userId: 'immich_id',
       userEmail: 'immich@test.com',
       firstName: 'immich_first_name',
@@ -246,7 +265,7 @@ export const loginResponseStub = {
       shouldChangePassword: false,
     },
     cookie: [
-      'immich_access_token=signed-jwt; HttpOnly; Path=/; Max-Age=604800; SameSite=Strict;',
+      'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=604800; SameSite=Strict;',
       'immich_auth_type=password; HttpOnly; Path=/; Max-Age=604800; SameSite=Strict;',
     ],
   },
diff --git a/server/libs/domain/test/user-token.repository.mock.ts b/server/libs/domain/test/user-token.repository.mock.ts
new file mode 100644
index 0000000000..593f96c0f4
--- /dev/null
+++ b/server/libs/domain/test/user-token.repository.mock.ts
@@ -0,0 +1,9 @@
+import { IUserTokenRepository } from '../src';
+
+export const newUserTokenRepositoryMock = (): jest.Mocked<IUserTokenRepository> => {
+  return {
+    create: jest.fn(),
+    delete: jest.fn(),
+    get: jest.fn(),
+  };
+};
diff --git a/server/libs/infra/src/auth/crypto.repository.ts b/server/libs/infra/src/auth/crypto.repository.ts
index 83d99a3ec8..59c7c310ef 100644
--- a/server/libs/infra/src/auth/crypto.repository.ts
+++ b/server/libs/infra/src/auth/crypto.repository.ts
@@ -1,22 +1,16 @@
 import { ICryptoRepository } from '@app/domain';
 import { Injectable } from '@nestjs/common';
-import { JwtService, JwtVerifyOptions } from '@nestjs/jwt';
 import { compareSync, hash } from 'bcrypt';
-import { randomBytes } from 'crypto';
+import { randomBytes, createHash } from 'crypto';
 
 @Injectable()
 export class CryptoRepository implements ICryptoRepository {
-  constructor(private jwtService: JwtService) {}
-
   randomBytes = randomBytes;
-  hash = hash;
-  compareSync = compareSync;
 
-  signJwt(payload: string | Buffer | object) {
-    return this.jwtService.sign(payload);
-  }
+  hashBcrypt = hash;
+  compareBcrypt = compareSync;
 
-  verifyJwtAsync<T extends object = any>(token: string, options?: JwtVerifyOptions): Promise<T> {
-    return this.jwtService.verifyAsync(token, options);
+  hashSha256(value: string) {
+    return createHash('sha256').update(value).digest('base64');
   }
 }
diff --git a/server/libs/infra/src/db/config/database.config.ts b/server/libs/infra/src/db/config/database.config.ts
index 84765d0afb..b79ae32cb7 100644
--- a/server/libs/infra/src/db/config/database.config.ts
+++ b/server/libs/infra/src/db/config/database.config.ts
@@ -5,11 +5,11 @@ const url = process.env.DB_URL;
 const urlOrParts = url
   ? { url }
   : {
-      host: process.env.DB_HOSTNAME || 'immich_postgres',
+      host: process.env.DB_HOSTNAME || 'localhost',
       port: parseInt(process.env.DB_PORT || '5432'),
-      username: process.env.DB_USERNAME,
-      password: process.env.DB_PASSWORD,
-      database: process.env.DB_DATABASE_NAME,
+      username: process.env.DB_USERNAME || 'postgres',
+      password: process.env.DB_PASSWORD || 'postgres',
+      database: process.env.DB_DATABASE_NAME || 'immich',
     };
 
 export const databaseConfig: PostgresConnectionOptions = {
diff --git a/server/libs/infra/src/db/entities/index.ts b/server/libs/infra/src/db/entities/index.ts
index 81073d4ce1..3ea8abcb15 100644
--- a/server/libs/infra/src/db/entities/index.ts
+++ b/server/libs/infra/src/db/entities/index.ts
@@ -9,4 +9,5 @@ export * from './system-config.entity';
 export * from './tag.entity';
 export * from './user-album.entity';
 export * from './user.entity';
+export * from './user-token.entity';
 export * from './shared-link.entity';
diff --git a/server/libs/infra/src/db/entities/user-token.entity.ts b/server/libs/infra/src/db/entities/user-token.entity.ts
new file mode 100644
index 0000000000..3418f2c823
--- /dev/null
+++ b/server/libs/infra/src/db/entities/user-token.entity.ts
@@ -0,0 +1,20 @@
+import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import { UserEntity } from './user.entity';
+
+@Entity('user_token')
+export class UserTokenEntity {
+  @PrimaryGeneratedColumn('uuid')
+  id!: string;
+
+  @Column({ select: false })
+  token!: string;
+
+  @ManyToOne(() => UserEntity)
+  user!: UserEntity;
+
+  @CreateDateColumn({ type: 'timestamptz' })
+  createdAt!: string;
+
+  @UpdateDateColumn({ type: 'timestamptz' })
+  updatedAt!: string;
+}
diff --git a/server/libs/infra/src/db/migrations/1674342044239-CreateUserTokenEntity.ts b/server/libs/infra/src/db/migrations/1674342044239-CreateUserTokenEntity.ts
new file mode 100644
index 0000000000..e289787f91
--- /dev/null
+++ b/server/libs/infra/src/db/migrations/1674342044239-CreateUserTokenEntity.ts
@@ -0,0 +1,16 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class CreateUserTokenEntity1674342044239 implements MigrationInterface {
+    name = 'CreateUserTokenEntity1674342044239'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TABLE "user_token" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "userId" uuid, CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`ALTER TABLE "user_token" ADD CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "user_token" DROP CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918"`);
+        await queryRunner.query(`DROP TABLE "user_token"`);
+    }
+
+}
diff --git a/server/libs/infra/src/db/migrations/1674774248319-TruncateAPIKeys.ts b/server/libs/infra/src/db/migrations/1674774248319-TruncateAPIKeys.ts
new file mode 100644
index 0000000000..efbb5c41af
--- /dev/null
+++ b/server/libs/infra/src/db/migrations/1674774248319-TruncateAPIKeys.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm"
+
+export class TruncateAPIKeys1674774248319 implements MigrationInterface {
+    name = 'TruncateAPIKeys1674774248319'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`TRUNCATE TABLE "api_keys"`);
+    }
+
+    public async down(): Promise<void> {
+        //noop
+    }
+
+}
diff --git a/server/libs/infra/src/db/repository/api-key.repository.ts b/server/libs/infra/src/db/repository/api-key.repository.ts
index 18ee6e6925..35119d2d7c 100644
--- a/server/libs/infra/src/db/repository/api-key.repository.ts
+++ b/server/libs/infra/src/db/repository/api-key.repository.ts
@@ -21,14 +21,14 @@ export class APIKeyRepository implements IKeyRepository {
     await this.repository.delete({ userId, id });
   }
 
-  getKey(id: number): Promise<APIKeyEntity | null> {
+  getKey(hashedToken: string): Promise<APIKeyEntity | null> {
     return this.repository.findOne({
       select: {
         id: true,
         key: true,
         userId: true,
       },
-      where: { id },
+      where: { key: hashedToken },
       relations: {
         user: true,
       },
diff --git a/server/libs/infra/src/db/repository/index.ts b/server/libs/infra/src/db/repository/index.ts
index 899bc21760..056c960573 100644
--- a/server/libs/infra/src/db/repository/index.ts
+++ b/server/libs/infra/src/db/repository/index.ts
@@ -1,3 +1,4 @@
 export * from './api-key.repository';
 export * from './shared-link.repository';
 export * from './user.repository';
+export * from './user-token.repository';
diff --git a/server/libs/infra/src/db/repository/user-token.repository.ts b/server/libs/infra/src/db/repository/user-token.repository.ts
new file mode 100644
index 0000000000..1fd9d03639
--- /dev/null
+++ b/server/libs/infra/src/db/repository/user-token.repository.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { UserTokenEntity } from '@app/infra/db/entities/user-token.entity';
+import { IUserTokenRepository } from '@app/domain/user-token';
+
+@Injectable()
+export class UserTokenRepository implements IUserTokenRepository {
+  constructor(
+    @InjectRepository(UserTokenEntity)
+    private userTokenRepository: Repository<UserTokenEntity>,
+  ) {}
+
+  async get(userToken: string): Promise<UserTokenEntity | null> {
+    return this.userTokenRepository.findOne({ where: { token: userToken }, relations: { user: true } });
+  }
+
+  async create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
+    return this.userTokenRepository.save(userToken);
+  }
+
+  async delete(userToken: string): Promise<void> {
+    await this.userTokenRepository.delete(userToken);
+  }
+}
diff --git a/server/libs/infra/src/infra.module.ts b/server/libs/infra/src/infra.module.ts
index 67248c4eb4..0f37221f2b 100644
--- a/server/libs/infra/src/infra.module.ts
+++ b/server/libs/infra/src/infra.module.ts
@@ -7,17 +7,17 @@ import {
   IUserRepository,
   QueueName,
 } from '@app/domain';
-import { databaseConfig, UserEntity } from './db';
+import { databaseConfig, UserEntity, UserTokenEntity } from './db';
 import { BullModule } from '@nestjs/bull';
 import { Global, Module, Provider } from '@nestjs/common';
-import { JwtModule } from '@nestjs/jwt';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { APIKeyEntity, SharedLinkEntity, SystemConfigEntity, UserRepository } from './db';
 import { APIKeyRepository, SharedLinkRepository } from './db/repository';
-import { jwtConfig } from '@app/domain';
 import { CryptoRepository } from './auth/crypto.repository';
 import { SystemConfigRepository } from './db/repository/system-config.repository';
 import { JobRepository } from './job';
+import { IUserTokenRepository } from '@app/domain/user-token';
+import { UserTokenRepository } from '@app/infra/db/repository/user-token.repository';
 
 const providers: Provider[] = [
   { provide: ICryptoRepository, useClass: CryptoRepository },
@@ -26,14 +26,14 @@ const providers: Provider[] = [
   { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
   { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
   { provide: IUserRepository, useClass: UserRepository },
+  { provide: IUserTokenRepository, useClass: UserTokenRepository },
 ];
 
 @Global()
 @Module({
   imports: [
-    JwtModule.register(jwtConfig),
     TypeOrmModule.forRoot(databaseConfig),
-    TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SharedLinkEntity, SystemConfigEntity]),
+    TypeOrmModule.forFeature([APIKeyEntity, UserEntity, SharedLinkEntity, SystemConfigEntity, UserTokenEntity]),
     BullModule.forRootAsync({
       useFactory: async () => ({
         prefix: 'immich_bull',
@@ -64,6 +64,6 @@ const providers: Provider[] = [
     ),
   ],
   providers: [...providers],
-  exports: [...providers, BullModule, JwtModule],
+  exports: [...providers, BullModule],
 })
 export class InfraModule {}
diff --git a/server/package-lock.json b/server/package-lock.json
index 500c72d19c..50f738191f 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -13,7 +13,6 @@
         "@nestjs/common": "^9.2.1",
         "@nestjs/config": "^2.2.0",
         "@nestjs/core": "^9.2.1",
-        "@nestjs/jwt": "^10.0.1",
         "@nestjs/mapped-types": "1.2.0",
         "@nestjs/passport": "^9.0.0",
         "@nestjs/platform-express": "^9.2.1",
@@ -50,7 +49,6 @@
         "passport": "^0.6.0",
         "passport-custom": "^1.1.1",
         "passport-http-header-strategy": "^1.1.0",
-        "passport-jwt": "^4.0.0",
         "pg": "^8.8.0",
         "redis": "^4.5.1",
         "reflect-metadata": "^0.1.13",
@@ -83,7 +81,6 @@
         "@types/multer": "^1.4.7",
         "@types/mv": "^2.1.2",
         "@types/node": "^16.0.0",
-        "@types/passport-jwt": "^3.0.6",
         "@types/sharp": "^0.30.2",
         "@types/supertest": "^2.0.11",
         "@typescript-eslint/eslint-plugin": "^5.48.1",
@@ -1521,18 +1518,6 @@
         "uuid": "dist/bin/uuid"
       }
     },
-    "node_modules/@nestjs/jwt": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.1.tgz",
-      "integrity": "sha512-LwXBKVYHnFeX6GH/Wt0WDjsWCmNDC6tEdLlwNMAvJgYp+TkiCpEmQLkgRpifdUE29mvYSbjSnVs2kW2ob935NA==",
-      "dependencies": {
-        "@types/jsonwebtoken": "8.5.9",
-        "jsonwebtoken": "9.0.0"
-      },
-      "peerDependencies": {
-        "@nestjs/common": "^8.0.0 || ^9.0.0"
-      }
-    },
     "node_modules/@nestjs/mapped-types": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz",
@@ -2714,14 +2699,6 @@
       "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
       "dev": true
     },
-    "node_modules/@types/jsonwebtoken": {
-      "version": "8.5.9",
-      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz",
-      "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==",
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
     "node_modules/@types/lodash": {
       "version": "4.14.178",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
@@ -2770,36 +2747,6 @@
       "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
       "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
     },
-    "node_modules/@types/passport": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz",
-      "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==",
-      "dev": true,
-      "dependencies": {
-        "@types/express": "*"
-      }
-    },
-    "node_modules/@types/passport-jwt": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
-      "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
-      "dev": true,
-      "dependencies": {
-        "@types/express": "*",
-        "@types/jsonwebtoken": "*",
-        "@types/passport-strategy": "*"
-      }
-    },
-    "node_modules/@types/passport-strategy": {
-      "version": "0.2.35",
-      "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
-      "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
-      "dev": true,
-      "dependencies": {
-        "@types/express": "*",
-        "@types/passport": "*"
-      }
-    },
     "node_modules/@types/prettier": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz",
@@ -3973,11 +3920,6 @@
         "node": "*"
       }
     },
-    "node_modules/buffer-equal-constant-time": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
-      "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
-    },
     "node_modules/buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -5019,14 +4961,6 @@
         "safer-buffer": "^2.1.0"
       }
     },
-    "node_modules/ecdsa-sig-formatter": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
-      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
-      "dependencies": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7895,21 +7829,6 @@
         "graceful-fs": "^4.1.6"
       }
     },
-    "node_modules/jsonwebtoken": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
-      "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
-      "dependencies": {
-        "jws": "^3.2.2",
-        "lodash": "^4.17.21",
-        "ms": "^2.1.1",
-        "semver": "^7.3.8"
-      },
-      "engines": {
-        "node": ">=12",
-        "npm": ">=6"
-      }
-    },
     "node_modules/jsprim": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -7924,25 +7843,6 @@
         "node": ">=0.6.0"
       }
     },
-    "node_modules/jwa": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
-      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
-      "dependencies": {
-        "buffer-equal-constant-time": "1.0.1",
-        "ecdsa-sig-formatter": "1.0.11",
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "node_modules/jws": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
-      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
-      "dependencies": {
-        "jwa": "^1.4.1",
-        "safe-buffer": "^5.0.1"
-      }
-    },
     "node_modules/kdt": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz",
@@ -9005,15 +8905,6 @@
         "passport-strategy": "^1.0.0"
       }
     },
-    "node_modules/passport-jwt": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
-      "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
-      "dependencies": {
-        "jsonwebtoken": "^9.0.0",
-        "passport-strategy": "^1.0.0"
-      }
-    },
     "node_modules/passport-strategy": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -12769,15 +12660,6 @@
         }
       }
     },
-    "@nestjs/jwt": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.1.tgz",
-      "integrity": "sha512-LwXBKVYHnFeX6GH/Wt0WDjsWCmNDC6tEdLlwNMAvJgYp+TkiCpEmQLkgRpifdUE29mvYSbjSnVs2kW2ob935NA==",
-      "requires": {
-        "@types/jsonwebtoken": "8.5.9",
-        "jsonwebtoken": "9.0.0"
-      }
-    },
     "@nestjs/mapped-types": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.0.tgz",
@@ -13715,14 +13597,6 @@
       "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
       "dev": true
     },
-    "@types/jsonwebtoken": {
-      "version": "8.5.9",
-      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz",
-      "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==",
-      "requires": {
-        "@types/node": "*"
-      }
-    },
     "@types/lodash": {
       "version": "4.14.178",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
@@ -13771,36 +13645,6 @@
       "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
       "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
     },
-    "@types/passport": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz",
-      "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==",
-      "dev": true,
-      "requires": {
-        "@types/express": "*"
-      }
-    },
-    "@types/passport-jwt": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
-      "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
-      "dev": true,
-      "requires": {
-        "@types/express": "*",
-        "@types/jsonwebtoken": "*",
-        "@types/passport-strategy": "*"
-      }
-    },
-    "@types/passport-strategy": {
-      "version": "0.2.35",
-      "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
-      "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
-      "dev": true,
-      "requires": {
-        "@types/express": "*",
-        "@types/passport": "*"
-      }
-    },
     "@types/prettier": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz",
@@ -14727,11 +14571,6 @@
       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
       "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
     },
-    "buffer-equal-constant-time": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
-      "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
-    },
     "buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -15545,14 +15384,6 @@
         "safer-buffer": "^2.1.0"
       }
     },
-    "ecdsa-sig-formatter": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
-      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
-      "requires": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -17690,17 +17521,6 @@
         "universalify": "^2.0.0"
       }
     },
-    "jsonwebtoken": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
-      "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
-      "requires": {
-        "jws": "^3.2.2",
-        "lodash": "^4.17.21",
-        "ms": "^2.1.1",
-        "semver": "^7.3.8"
-      }
-    },
     "jsprim": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -17712,25 +17532,6 @@
         "verror": "1.10.0"
       }
     },
-    "jwa": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
-      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
-      "requires": {
-        "buffer-equal-constant-time": "1.0.1",
-        "ecdsa-sig-formatter": "1.0.11",
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "jws": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
-      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
-      "requires": {
-        "jwa": "^1.4.1",
-        "safe-buffer": "^5.0.1"
-      }
-    },
     "kdt": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz",
@@ -18555,15 +18356,6 @@
         "passport-strategy": "^1.0.0"
       }
     },
-    "passport-jwt": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
-      "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
-      "requires": {
-        "jsonwebtoken": "^9.0.0",
-        "passport-strategy": "^1.0.0"
-      }
-    },
     "passport-strategy": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
diff --git a/server/package.json b/server/package.json
index b433840221..d906f2b1bd 100644
--- a/server/package.json
+++ b/server/package.json
@@ -29,6 +29,10 @@
     "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
     "test:e2e": "jest --config ./apps/immich/test/jest-e2e.json --runInBand",
     "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
+    "typeorm:migrations:generate": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./libs/infra/src/db/config/database.config.ts",
+    "typeorm:migrations:run": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:run -d ./libs/infra/src/db/config/database.config.ts",
+    "typeorm:migrations:revert": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:revert -d ./libs/infra/src/db/config/database.config.ts",
+    "typeorm:schema:drop": "node --require ts-node/register ./node_modules/typeorm/cli.js schema:drop -d ./libs/infra/src/db/config/database.config.ts",
     "api:typescript": "bash ./bin/generate-open-api.sh web",
     "api:dart": "bash ./bin/generate-open-api.sh mobile",
     "api:generate": "bash ./bin/generate-open-api.sh"
@@ -38,7 +42,6 @@
     "@nestjs/common": "^9.2.1",
     "@nestjs/config": "^2.2.0",
     "@nestjs/core": "^9.2.1",
-    "@nestjs/jwt": "^10.0.1",
     "@nestjs/mapped-types": "1.2.0",
     "@nestjs/passport": "^9.0.0",
     "@nestjs/platform-express": "^9.2.1",
@@ -75,7 +78,6 @@
     "passport": "^0.6.0",
     "passport-custom": "^1.1.1",
     "passport-http-header-strategy": "^1.1.0",
-    "passport-jwt": "^4.0.0",
     "pg": "^8.8.0",
     "redis": "^4.5.1",
     "reflect-metadata": "^0.1.13",
@@ -105,7 +107,6 @@
     "@types/multer": "^1.4.7",
     "@types/mv": "^2.1.2",
     "@types/node": "^16.0.0",
-    "@types/passport-jwt": "^3.0.6",
     "@types/sharp": "^0.30.2",
     "@types/supertest": "^2.0.11",
     "@typescript-eslint/eslint-plugin": "^5.48.1",