diff --git a/.github/.nvmrc b/.github/.nvmrc
new file mode 100644
index 0000000000..7d41c735d7
--- /dev/null
+++ b/.github/.nvmrc
@@ -0,0 +1 @@
+22.14.0
diff --git a/.github/DISCUSSION_TEMPLATE/feature-request.yaml b/.github/DISCUSSION_TEMPLATE/feature-request.yaml
index 7a260188ea..8a2358cc2b 100644
--- a/.github/DISCUSSION_TEMPLATE/feature-request.yaml
+++ b/.github/DISCUSSION_TEMPLATE/feature-request.yaml
@@ -1,5 +1,5 @@
-title: "[Feature] feature-name-goes-here"
-labels: ["feature"]
+title: '[Feature] feature-name-goes-here'
+labels: ['feature']
 
 body:
   - type: markdown
@@ -13,7 +13,7 @@ body:
     attributes:
       label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
       options:
-        - label: "Yes"
+        - label: 'Yes'
           required: true
 
   - type: textarea
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index c4e1cc2bf1..15274f75e8 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -5,7 +5,7 @@ body:
     attributes:
       label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
       options:
-        - label: "Yes"
+        - label: 'Yes'
           required: true
 
   - type: markdown
@@ -84,7 +84,7 @@ body:
     id: repro
     attributes:
       label: Reproduction steps
-      description: "How do you trigger this bug? Please walk us through it step by step."
+      description: 'How do you trigger this bug? Please walk us through it step by step.'
       value: |
         1.
         2.
@@ -97,12 +97,13 @@ body:
     id: logs
     attributes:
       label: Relevant log output
-      description: Please copy and paste any relevant logs below. (code formatting is
+      description:
+        Please copy and paste any relevant logs below. (code formatting is
         enabled, no need for backticks)
       render: shell
     validations:
       required: false
-      
+
   - type: textarea
     attributes:
       label: Additional information
diff --git a/.github/package-lock.json b/.github/package-lock.json
new file mode 100644
index 0000000000..423661befb
--- /dev/null
+++ b/.github/package-lock.json
@@ -0,0 +1,28 @@
+{
+  "name": ".github",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "devDependencies": {
+        "prettier": "^3.5.3"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+      "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    }
+  }
+}
diff --git a/.github/package.json b/.github/package.json
new file mode 100644
index 0000000000..1cb0262c74
--- /dev/null
+++ b/.github/package.json
@@ -0,0 +1,9 @@
+{
+  "scripts": {
+    "format": "prettier --check .",
+    "format:fix": "prettier --write ."
+  },
+  "devDependencies": {
+    "prettier": "^3.5.3"
+  }
+}
diff --git a/.github/release.yml b/.github/release.yml
index c549ead475..108daaf40f 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -1,33 +1,33 @@
-changelog:
-  categories:
-    - title: 🚨 Breaking Changes
-      labels:
-        - changelog:breaking-change
-
-    - title: đŸĢĨ Deprecated Changes
-      labels:
-        - changelog:deprecated
-
-    - title: 🔒 Security
-      labels:
-        - changelog:security
-
-    - title: 🚀 Features
-      labels:
-        - changelog:feature
-
-    - title: 🌟 Enhancements
-      labels:
-        - changelog:enhancement
-
-    - title: 🐛 Bug fixes
-      labels:
-        - changelog:bugfix
-
-    - title: 📚 Documentation
-      labels:
-        - changelog:documentation
-
-    - title: 🌐 Translations
-      labels:
-        - changelog:translation
+changelog:
+  categories:
+    - title: 🚨 Breaking Changes
+      labels:
+        - changelog:breaking-change
+
+    - title: đŸĢĨ Deprecated Changes
+      labels:
+        - changelog:deprecated
+
+    - title: 🔒 Security
+      labels:
+        - changelog:security
+
+    - title: 🚀 Features
+      labels:
+        - changelog:feature
+
+    - title: 🌟 Enhancements
+      labels:
+        - changelog:enhancement
+
+    - title: 🐛 Bug fixes
+      labels:
+        - changelog:bugfix
+
+    - title: 📚 Documentation
+      labels:
+        - changelog:documentation
+
+    - title: 🌐 Translations
+      labels:
+        - changelog:translation
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 3c6dd61548..0263e4bce8 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -9,14 +9,14 @@
 # the `language` matrix defined below to confirm you have the correct set of
 # supported CodeQL languages.
 #
-name: "CodeQL"
+name: 'CodeQL'
 
 on:
   push:
-    branches: [ "main" ]
+    branches: ['main']
   pull_request:
     # The branches below must be a subset of the branches above
-    branches: [ "main" ]
+    branches: ['main']
   schedule:
     - cron: '20 13 * * 1'
 
@@ -36,43 +36,42 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        language: [ 'javascript', 'python' ]
+        language: ['javascript', 'python']
         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
         # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
 
     steps:
-    - name: Checkout repository
-      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+      - name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
 
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3
-      with:
-        languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
+      # Initializes the CodeQL tools for scanning.
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3
+        with:
+          languages: ${{ matrix.language }}
+          # If you wish to specify custom queries, you can do so here or in a config file.
+          # By default, queries listed here will override any specified in a config file.
+          # Prefix the list here with "+" to use these queries and those in the config file.
 
-        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
-        # queries: security-extended,security-and-quality
+          # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+          # queries: security-extended,security-and-quality
 
+      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+      # If this step fails, then you should remove it and run the build manually (see below)
+      - name: Autobuild
+        uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3
 
-    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3
+      # â„šī¸ Command-line programs to run using the OS shell.
+      # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
 
-    # â„šī¸ Command-line programs to run using the OS shell.
-    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+      #   If the Autobuild fails above, remove it and uncomment the following three lines.
+      #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
 
-    #   If the Autobuild fails above, remove it and uncomment the following three lines.
-    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+      # - run: |
+      #   echo "Run, Build Application using script"
+      #   ./location_of_script_within_repo/buildscript.sh
 
-    # - run: |
-    #   echo "Run, Build Application using script"
-    #   ./location_of_script_within_repo/buildscript.sh
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3
-      with:
-        category: "/language:${{matrix.language}}"
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3
+        with:
+          category: '/language:${{matrix.language}}'
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 6b3ddba01d..f33c0c4c03 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -1,7 +1,7 @@
 name: Docs deploy
 on:
   workflow_run:
-    workflows: ["Docs build"]
+    workflows: ['Docs build']
     types:
       - completed
 
@@ -140,10 +140,10 @@ jobs:
           TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
         uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
         with:
-          tg_version: "0.58.12"
-          tofu_version: "1.7.1"
-          tg_dir: "deployment/modules/cloudflare/docs"
-          tg_command: "apply"
+          tg_version: '0.58.12'
+          tofu_version: '1.7.1'
+          tg_dir: 'deployment/modules/cloudflare/docs'
+          tg_command: 'apply'
 
       - name: Deploy Docs Subdomain Output
         id: docs-output
@@ -155,10 +155,10 @@ jobs:
           TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
         uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
         with:
-          tg_version: "0.58.12"
-          tofu_version: "1.7.1"
-          tg_dir: "deployment/modules/cloudflare/docs"
-          tg_command: "output -json"
+          tg_version: '0.58.12'
+          tofu_version: '1.7.1'
+          tg_dir: 'deployment/modules/cloudflare/docs'
+          tg_command: 'output -json'
 
       - name: Output Cleaning
         id: clean
@@ -172,8 +172,8 @@ jobs:
           apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
           accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
           projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
-          workingDirectory: "docs"
-          directory: "build"
+          workingDirectory: 'docs'
+          directory: 'build'
           branch: ${{ steps.parameters.outputs.name }}
           wranglerVersion: '3'
 
diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml
index 2f8b218093..99499528b6 100644
--- a/.github/workflows/docs-destroy.yml
+++ b/.github/workflows/docs-destroy.yml
@@ -13,17 +13,17 @@ jobs:
 
       - name: Destroy Docs Subdomain
         env:
-          TF_VAR_prefix_name: "pr-${{ github.event.number }}"
-          TF_VAR_prefix_event_type: "pr"
+          TF_VAR_prefix_name: 'pr-${{ github.event.number }}'
+          TF_VAR_prefix_event_type: 'pr'
           CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
           CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
           TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
         uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
         with:
-          tg_version: "0.58.12"
-          tofu_version: "1.7.1"
-          tg_dir: "deployment/modules/cloudflare/docs"
-          tg_command: "destroy -refresh=false"
+          tg_version: '0.58.12'
+          tofu_version: '1.7.1'
+          tg_dir: 'deployment/modules/cloudflare/docs'
+          tg_command: 'destroy -refresh=false'
 
       - name: Comment
         uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml
index 12f7875197..69a0a8f611 100644
--- a/.github/workflows/fix-format.yml
+++ b/.github/workflows/fix-format.yml
@@ -49,4 +49,3 @@ jobs:
               repo: context.repo.repo,
               name: 'fix:formatting'
             })
-
diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml
index 1806b0a699..247c625a96 100644
--- a/.github/workflows/pr-label-validation.yml
+++ b/.github/workflows/pr-label-validation.yml
@@ -17,6 +17,6 @@ jobs:
           mode: exactly
           count: 1
           use_regex: true
-          labels: "changelog:.*"
+          labels: 'changelog:.*'
           add_comment: true
-          message: "Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label."
+          message: 'Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label.'
diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
index b1cdfcf47d..1b43c89889 100644
--- a/.github/workflows/pr-labeler.yml
+++ b/.github/workflows/pr-labeler.yml
@@ -1,6 +1,6 @@
-name: "Pull Request Labeler"
+name: 'Pull Request Labeler'
 on:
-- pull_request_target
+  - pull_request_target
 
 jobs:
   labeler:
@@ -9,4 +9,4 @@ jobs:
       pull-requests: write
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
+      - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
diff --git a/.github/workflows/pr-require-conventional-commit.yml b/.github/workflows/pr-require-conventional-commit.yml
index d4bd44ec43..c54b10a8ad 100644
--- a/.github/workflows/pr-require-conventional-commit.yml
+++ b/.github/workflows/pr-require-conventional-commit.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: PR Conventional Commit Validation
-        uses:  ytanikin/PRConventionalCommits@1.3.0
+        uses: ytanikin/PRConventionalCommits@1.3.0
         with:
           task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
           add_label: 'false'
diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml
index be244f2e6d..447a309a2e 100644
--- a/.github/workflows/preview-label.yaml
+++ b/.github/workflows/preview-label.yaml
@@ -13,8 +13,8 @@ jobs:
     steps:
       - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2
         with:
-          message-id: "preview-status"
-          message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"
+          message-id: 'preview-status'
+          message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/'
 
   remove-label:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b3df4931ff..d649e4f9b3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,6 +21,7 @@ jobs:
       should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
       should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
       should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
+      should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
     steps:
       - name: Checkout code
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -45,6 +46,8 @@ jobs:
               - 'machine-learning/**'
             workflow:
               - '.github/workflows/test.yml'
+            .github:
+              - '.github/**'
 
       - name: Check if we should force jobs to run
         id: should_force
@@ -403,6 +406,31 @@ jobs:
         run: |
           uv run pytest app --cov=app --cov-report term-missing
 
+  github-files-formatting:
+    name: .github Files Formatting
+    needs: pre-job
+    if: ${{ needs.pre-job.outputs['should_run_.github'] == 'true' }}
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: ./.github
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Setup Node
+        uses: actions/setup-node@v4
+        with:
+          node-version-file: './.github/.nvmrc'
+
+      - name: Run npm install
+        run: npm ci
+
+      - name: Run formatter
+        run: npm run format
+        if: ${{ !cancelled() }}
+
   shellcheck:
     name: ShellCheck
     runs-on: ubuntu-latest
diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml
index de43cda1f1..69dce3ac41 100644
--- a/.github/workflows/weblate-lock.yml
+++ b/.github/workflows/weblate-lock.yml
@@ -20,13 +20,13 @@ jobs:
               - 'i18n/!(en)**\.json'
       - name: Debug
         run: |
-            echo "Should run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}"
-            echo "Found i18n paths: ${{ steps.found_paths.outputs.i18n }}"
-            echo "Head ref: ${{ github.head_ref }}"
+          echo "Should run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}"
+          echo "Found i18n paths: ${{ steps.found_paths.outputs.i18n }}"
+          echo "Head ref: ${{ github.head_ref }}"
 
   enforce-lock:
     name: Check Weblate Lock
-    needs: [ pre-job ]
+    needs: [pre-job]
     runs-on: ubuntu-latest
     if: ${{ needs.pre-job.outputs.should_run == 'true' }}
     steps:
@@ -45,7 +45,7 @@ jobs:
         run: exit 1
   success-check-lock:
     name: Weblate Lock Check Success
-    needs: [ enforce-lock ]
+    needs: [enforce-lock]
     runs-on: ubuntu-latest
     if: always()
     steps:
diff --git a/Makefile b/Makefile
index 0899d82d24..e15faa8051 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@ attach-server:
 renovate:
   LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
 
-MODULES = e2e server web cli sdk docs
+MODULES = e2e server web cli sdk docs .github
 
 audit-%:
 	npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
@@ -77,14 +77,14 @@ test-medium:
 test-medium-dev:
 	docker exec -it immich_server /bin/sh -c "npm run test:medium"
 
-build-all: $(foreach M,$(filter-out e2e,$(MODULES)),build-$M) ;
+build-all: $(foreach M,$(filter-out e2e .github,$(MODULES)),build-$M) ;
 install-all: $(foreach M,$(MODULES),install-$M) ;
-check-all: $(foreach M,$(filter-out sdk cli docs,$(MODULES)),check-$M) ;
-lint-all: $(foreach M,$(filter-out sdk docs,$(MODULES)),lint-$M) ;
+check-all: $(foreach M,$(filter-out sdk cli docs .github,$(MODULES)),check-$M) ;
+lint-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),lint-$M) ;
 format-all: $(foreach M,$(filter-out sdk,$(MODULES)),format-$M) ;
 audit-all:  $(foreach M,$(MODULES),audit-$M) ;
 hygiene-all: lint-all format-all check-all sql audit-all;
-test-all: $(foreach M,$(filter-out sdk docs,$(MODULES)),test-$M) ;
+test-all: $(foreach M,$(filter-out sdk docs .github,$(MODULES)),test-$M) ;
 
 clean:
 	find . -name "node_modules" -type d -prune -exec rm -rf '{}' +