diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 57e3f3dc139..2ea0b316a31 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -74,6 +74,8 @@ jobs:
   build_linux_flatpak:
     env:
       APP_ID: dev.lizardbyte.app.Sunshine
+      NODE_VERSION: "20"
+      PLATFORM_VERSION: "23.08"
     name: Linux Flatpak
     runs-on: ubuntu-22.04
     needs: [setup_release, setup_flatpak_matrix]
@@ -97,10 +99,30 @@ jobs:
         with:
           submodules: recursive
 
+      - name: Setup node
+        id: node
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+
+      - name: Install npm dependencies
+        run: |
+          npm install --package-lock-only
+
+      - name: Debug package-lock.json
+        run: |
+          cat package-lock.json
+
+      - name: Setup python
+        id: python
+        uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+
       - name: Setup Dependencies Linux Flatpak
-        env:
-          PLATFORM_VERSION: "22.08"
         run: |
+          python -m pip install ./packaging/linux/flatpak/deps/flatpak-builder-tools/node
+
           sudo apt-get update -y
           sudo apt-get install -y \
             cmake \
@@ -114,10 +136,20 @@ jobs:
             org.flatpak.Builder \
             org.freedesktop.Platform/${{ matrix.arch }}/${PLATFORM_VERSION} \
             org.freedesktop.Sdk/${{ matrix.arch }}/${PLATFORM_VERSION} \
-            org.freedesktop.Sdk.Extension.node18/${{ matrix.arch }}/${PLATFORM_VERSION} \
-            org.freedesktop.Sdk.Extension.vala/${{ matrix.arch }}/${PLATFORM_VERSION} \
+            org.freedesktop.Sdk.Extension.node${NODE_VERSION}/${{ matrix.arch }}/${PLATFORM_VERSION} \
             "
 
+          flatpak run org.flatpak.Builder --version
+
+      - name: flatpak node generator
+        # https://github.com/flatpak/flatpak-builder-tools/blob/master/node/README.md
+        run: |
+          flatpak-node-generator npm package-lock.json
+
+      - name: Debug generated-sources.json
+        run: |
+          cat generated-sources.json
+
       - name: Cache Flatpak build
         uses: actions/cache@v4
         with:
@@ -167,16 +199,29 @@ jobs:
       - name: Build Linux Flatpak
         working-directory: build
         run: |
-          sudo su $(whoami) -c "flatpak run org.flatpak.Builder --arch=${{ matrix.arch }} --repo=repo --force-clean \
+          sudo su $(whoami) -c "flatpak run org.flatpak.Builder \
+            --arch=${{ matrix.arch }} \
+            --force-clean \
+            --repo=repo \
+            --sandbox \
             --stop-at=cuda build-sunshine ${APP_ID}.yml"
           cp -r .flatpak-builder copy-of-flatpak-builder
-          sudo su $(whoami) -c "flatpak run org.flatpak.Builder --arch=${{ matrix.arch }} --repo=repo --force-clean \
+          sudo su $(whoami) -c "flatpak run org.flatpak.Builder \
+            --arch=${{ matrix.arch }} \
+            --force-clean \
+            --repo=repo \
+            --sandbox \
             build-sunshine ${APP_ID}.yml"
           rm -rf .flatpak-builder
           mv copy-of-flatpak-builder .flatpak-builder
-          sudo su $(whoami) -c "flatpak build-bundle --arch=${{ matrix.arch }} ./repo \
+          sudo su $(whoami) -c "flatpak build-bundle \
+            --arch=${{ matrix.arch }} \
+            ./repo \
             ../artifacts/sunshine_${{ matrix.arch }}.flatpak ${APP_ID}"
-          sudo su $(whoami) -c "flatpak build-bundle --runtime --arch=${{ matrix.arch }} ./repo \
+          sudo su $(whoami) -c "flatpak build-bundle \
+            --runtime \
+            --arch=${{ matrix.arch }} \
+            ./repo \
             ../artifacts/sunshine_debug_${{ matrix.arch }}.flatpak ${APP_ID}.Debug"
 
       - name: Lint Flatpak
@@ -229,6 +274,23 @@ jobs:
           # exit with the correct code
           exit $exit_code
 
+      - name: Package Flathub repo archive
+        # copy files required to generate the Flathub repo
+        if: ${{ matrix.arch == 'x86_64' }}
+        run: |
+          mkdir -p flathub/modules
+          cp ./build/generated-sources.json ./flathub/
+          cp ./build/package-lock.json ./flathub/
+          cp ./build/${APP_ID}.yml ./flathub/
+          cp ./build/${APP_ID}.metainfo.xml ./flathub/
+          cp ./packaging/linux/flatpak/README.md ./flathub/
+          cp ./packaging/linux/flatpak/flathub.json ./flathub/
+          cp -r ./packaging/linux/flatpak/modules/. ./flathub/modules/
+          # submodules will need to be handled in the workflow that creates the PR
+
+          # create the archive
+          tar -czf ./artifacts/flathub.tar.gz -C ./flathub .
+
       - name: Upload Artifacts
         uses: actions/upload-artifact@v4
         with:
diff --git a/.github/workflows/update-flathub-repo.yml b/.github/workflows/update-flathub-repo.yml
new file mode 100644
index 00000000000..15580f23847
--- /dev/null
+++ b/.github/workflows/update-flathub-repo.yml
@@ -0,0 +1,187 @@
+---
+# This action is a candidate to centrally manage in https://github.com/<organization>/.github/
+# If more Flathub applications are developed, consider moving this action to the organization's .github repository,
+# using the `flathub-pkg` repository label to identify repositories that should trigger this workflow.
+
+# Update Flathub on release events.
+
+name: Update flathub repo
+
+on:
+  release:
+    types: [released]
+
+concurrency:
+  group: "${{ github.workflow }}-${{ github.event.release.tag_name }}"
+  cancel-in-progress: true
+
+jobs:
+  update-flathub-repo:
+    env:
+      FLATHUB_PKG: dev.lizardbyte.app.${{ github.event.repository.name }}
+    if: >-
+      github.repository_owner == 'LizardByte'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check if flathub repo
+        env:
+          TOPIC: flathub-pkg
+        id: check-label
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const topic = process.env.TOPIC;
+            console.log(`Checking if repo has topic: ${topic}`);
+
+            const repoTopics = await github.rest.repos.getAllTopics({
+              owner: context.repo.owner,
+              repo: context.repo.repo
+            });
+            console.log(`Repo topics: ${repoTopics.data.names}`);
+
+            const hasTopic = repoTopics.data.names.includes(topic);
+            console.log(`Has topic: ${hasTopic}`);
+
+            core.setOutput('hasTopic', hasTopic);
+
+      - name: Check if latest GitHub release
+        id: check-release
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true'
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const latestRelease = await github.rest.repos.getLatestRelease({
+              owner: context.repo.owner,
+              repo: context.repo.repo
+            });
+
+            core.setOutput('isLatestRelease', latestRelease.data.tag_name === context.payload.release.tag_name);
+
+      - name: Checkout
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        uses: actions/checkout@v4
+
+      - name: Checkout flathub-repo
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        uses: actions/checkout@v4
+        with:
+          repository: "flathub/${{ env.FLATHUB_PKG }}"
+          path: "flathub/${{ env.FLATHUB_PKG }}"
+
+      - name: Clean up legacy files
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        working-directory: flathub/${{ env.FLATHUB_PKG }}
+        run: |
+          rm -rf ./*
+
+      - name: Copy github files
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        working-directory: flathub/${{ env.FLATHUB_PKG }}
+        run: |
+          mkdir -p .github/ISSUE_TEMPLATE
+
+          # sponsors
+          curl -sSL https://github.com/LizardByte/.github/raw/refs/heads/master/.github/FUNDING.yml \
+            -o .github/FUNDING.yml
+          # pull request template
+          curl -sSL https://github.com/LizardByte/.github/raw/refs/heads/master/.github/pull_request_template.md \
+            -o .github/pull_request_template.md
+          # issue config
+          curl -sSL https://github.com/LizardByte/.github/raw/refs/heads/master/.github/ISSUE_TEMPLATE/config.yml \
+            -o .github/ISSUE_TEMPLATE/config.yml
+
+      - name: Download release asset
+        id: download
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        uses: robinraju/release-downloader@v1.11
+        with:
+          repository: "${{ github.repository }}"
+          tag: "${{ github.event.release.tag_name }}"
+          fileName: "flathub.tar.gz"
+          tarBall: false
+          zipBall: false
+          out-file-path: "flathub/${{ env.FLATHUB_PKG }}"
+          extract: true
+
+      - name: Delete arhive
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        run: |
+          rm -f flathub/${{ env.FLATHUB_PKG }}/flathub.tar.gz
+
+      - name: Update metainfo.xml
+        id: update_metainfo
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        run: |
+          xml_file="flathub/${{ env.FLATHUB_PKG }}/${{ env.FLATHUB_PKG }}.metainfo.xml"
+
+          # Extract release information
+          version="${{ github.event.release.tag_name }}" && version="${version#v}"
+          date="${{ github.event.release.published_at }}" && date="${date%%T*}"
+          changelog="${{ github.event.release.body }}" && changelog="${changelog//&/&amp;}" && \
+            changelog="${changelog//</&lt;}" && changelog="${changelog//>/&gt;}"
+
+          # Store the old release information into a temp file to be used for precise replacement
+          tmpfile=$(mktemp)
+
+          # Match the existing <release> block, replace it with the new data
+          awk -v version="$version" -v date="$date" -v changelog="$changelog" '
+          BEGIN { replaced = 0 }
+          /<release version=.*>/ {
+            if (!replaced) {
+              print "<release version=\"" version "\" date=\"" date "\">"
+              print "<description><p>" changelog "</p></description>"
+              print "</release>"
+              replaced = 1
+            }
+          }
+          !/<release version=.*>/ && !/<\/release>/ { print $0 }
+          ' "$xml_file" > "$tmpfile"
+
+          # Move the updated file back to the original location
+          mv "$tmpfile" "$xml_file"
+
+      - name: Update submodule
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
+        run: |
+          # Get the current commit of the submodule in the main repository
+          git submodule update --init packaging/linux/flatpak/deps/shared-modules
+          cd ${{ github.workspace }}/packaging/linux/flatpak/deps/shared-modules
+          main_commit=$(git rev-parse HEAD)
+
+          # update submodules
+          cd ${{ github.workspace }}/flathub/${{ env.FLATHUB_PKG }}
+          git submodule update --init shared-modules
+          cd shared-modules
+          git checkout $main_commit
+
+      - name: Create/Update Pull Request
+        if: >-
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true' &&
+          fromJson(steps.download.outputs.downloaded_files)[0]
+        uses: peter-evans/create-pull-request@v7
+        with:
+          path: "flathub/${{ env.FLATHUB_PKG }}"
+          token: ${{ secrets.GH_BOT_TOKEN }}
+          commit-message: Update ${{ env.FLATHUB_PKG }} to ${{ github.event.release.tag_name }}
+          branch: bot/bump-${{ env.FLATHUB_PKG }}-${{ github.event.release.tag_name }}
+          delete-branch: true
+          title: "chore: Update ${{ env.FLATHUB_PKG }} to ${{ github.event.release.tag_name }}"
+          body: ${{ github.event.release.body }}
diff --git a/.github/workflows/update-homebrew-release.yml b/.github/workflows/update-homebrew-release.yml
index d6acf476e9e..16796cb8e7c 100644
--- a/.github/workflows/update-homebrew-release.yml
+++ b/.github/workflows/update-homebrew-release.yml
@@ -9,7 +9,7 @@ name: Update Homebrew release
 
 on:
   release:
-    types: [created, edited]
+    types: [released]
 
 concurrency:
   group: "${{ github.workflow }}-${{ github.event.release.tag_name }}"
@@ -18,14 +18,13 @@ concurrency:
 jobs:
   update-homebrew-release:
     if: >-
-      github.repository_owner == 'LizardByte' &&
-      !github.event.release.draft && !github.event.release.prerelease
+      github.repository_owner == 'LizardByte'
     runs-on: ubuntu-latest
     steps:
       - name: Check if Homebrew repo
         env:
           TOPIC: homebrew-pkg
-        id: check
+        id: check-label
         uses: actions/github-script@v7
         with:
           script: |
@@ -46,7 +45,7 @@ jobs:
       - name: Download release asset
         id: download
         if: >-
-          steps.check.outputs.hasTopic == 'true'
+          steps.check-label.outputs.hasTopic == 'true'
         uses: robinraju/release-downloader@v1.11
         with:
           repository: "${{ github.repository }}"
@@ -59,7 +58,7 @@ jobs:
 
       - name: Publish Homebrew Formula
         if: >-
-          steps.check.outputs.hasTopic == 'true' &&
+          steps.check-label.outputs.hasTopic == 'true' &&
           fromJson(steps.download.outputs.downloaded_files)[0]
         uses: LizardByte/homebrew-release-action@v2024.919.145818
         with:
diff --git a/.github/workflows/update-pacman-repo.yml b/.github/workflows/update-pacman-repo.yml
index 946bf07f664..c95f7cda36d 100644
--- a/.github/workflows/update-pacman-repo.yml
+++ b/.github/workflows/update-pacman-repo.yml
@@ -9,7 +9,7 @@ name: Update pacman repo
 
 on:
   release:
-    types: [created, edited]
+    types: [released]
 
 concurrency:
   group: "${{ github.workflow }}-${{ github.event.release.tag_name }}"
@@ -18,14 +18,13 @@ concurrency:
 jobs:
   update-homebrew-release:
     if: >-
-      github.repository_owner == 'LizardByte' &&
-      !github.event.release.draft && !github.event.release.prerelease
+      github.repository_owner == 'LizardByte'
     runs-on: ubuntu-latest
     steps:
       - name: Check if pacman repo
         env:
           TOPIC: pacman-pkg
-        id: check
+        id: check-label
         uses: actions/github-script@v7
         with:
           script: |
@@ -43,11 +42,10 @@ jobs:
 
             core.setOutput('hasTopic', hasTopic);
 
-      - name: Check if edited release is latest GitHub release
-        id: check
+      - name: Check if latest GitHub release
+        id: check-release
         if: >-
-          github.event_name == 'release' &&
-          github.event.action == 'edited'
+          steps.check-label.outputs.hasTopic == 'true'
         uses: actions/github-script@v7
         with:
           script: |
@@ -60,8 +58,8 @@ jobs:
 
       - name: Checkout pacman-repo
         if: >-
-          steps.check.outputs.hasTopic == 'true' &&
-          steps.check.outputs.isLatestRelease == 'true'
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
         uses: actions/checkout@v4
         with:
           repository: ${{ github.repository_owner }}/pacman-repo
@@ -69,16 +67,16 @@ jobs:
       - name: Prep
         id: prep
         if: >-
-          steps.check.outputs.hasTopic == 'true' &&
-          steps.check.outputs.isLatestRelease == 'true'
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
         run: |
           echo "pkg_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
 
       - name: Download release asset
         id: download
         if: >-
-          steps.check.outputs.hasTopic == 'true' &&
-          steps.check.outputs.isLatestRelease == 'true'
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true'
         uses: robinraju/release-downloader@v1.11
         with:
           repository: "${{ github.repository }}"
@@ -91,8 +89,8 @@ jobs:
 
       - name: Create/Update Pull Request
         if: >-
-          steps.check.outputs.hasTopic == 'true'&&
-          steps.check.outputs.isLatestRelease == 'true' &&
+          steps.check-label.outputs.hasTopic == 'true' &&
+          steps.check-release.outputs.isLatestRelease == 'true' &&
           fromJson(steps.download.outputs.downloaded_files)[0]
         uses: peter-evans/create-pull-request@v7
         with:
@@ -102,8 +100,7 @@ jobs:
           commit-message: Update ${{ github.repository }} to ${{ github.event.release.tag_name }}
           branch: bot/bump-${{ github.repository }}-${{ github.event.release.tag_name }}
           delete-branch: true
-          base: master
-          title: Update ${{ github.repository }} to ${{ github.event.release.tag_name }}
+          title: "chore: Update ${{ github.repository }} to ${{ github.event.release.tag_name }}"
           body: ${{ github.event.release.body }}
           labels: |
             auto-approve
diff --git a/.github/workflows/update-winget-release.yml b/.github/workflows/update-winget-release.yml
index 9e3278ecb81..c004dcdd605 100644
--- a/.github/workflows/update-winget-release.yml
+++ b/.github/workflows/update-winget-release.yml
@@ -9,7 +9,7 @@ name: Update Winget release
 
 on:
   release:
-    types: [created, edited]
+    types: [released]
 
 concurrency:
   group: "${{ github.workflow }}-${{ github.event.release.tag_name }}"
@@ -18,14 +18,13 @@ concurrency:
 jobs:
   update-winget-release:
     if: >-
-      github.repository_owner == 'LizardByte' &&
-      !github.event.release.draft && !github.event.release.prerelease
+      github.repository_owner == 'LizardByte'
     runs-on: ubuntu-latest
     steps:
       - name: Check if Winget repo
         env:
           TOPIC: winget-pkg
-        id: check
+        id: check-label
         uses: actions/github-script@v7
         with:
           script: |
@@ -46,7 +45,7 @@ jobs:
       - name: Download release asset
         id: download
         if: >-
-          steps.check.outputs.hasTopic == 'true'
+          steps.check-label.outputs.hasTopic == 'true'
         uses: robinraju/release-downloader@v1.11
         with:
           repository: "${{ github.repository }}"
@@ -59,7 +58,7 @@ jobs:
 
       - name: Release to WinGet
         if: >-
-          steps.check.outputs.hasTopic == 'true' &&
+          steps.check-label.outputs.hasTopic == 'true' &&
           fromJson(steps.download.outputs.downloaded_files)[0]
         uses: vedantmgoyal2009/winget-releaser@v2
         with:
diff --git a/.gitmodules b/.gitmodules
index 88198314dd0..ed6ec248750 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,7 @@
+[submodule "packaging/linux/flatpak/deps/flatpak-builder-tools"]
+	path = packaging/linux/flatpak/deps/flatpak-builder-tools
+	url = https://github.com/flatpak/flatpak-builder-tools.git
+	branch = master
 [submodule "packaging/linux/flatpak/deps/shared-modules"]
 	path = packaging/linux/flatpak/deps/shared-modules
 	url = https://github.com/flathub/shared-modules
diff --git a/README.md b/README.md
index b2ce7143392..6146157627d 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,10 @@
 [![GitHub stars](https://img.shields.io/github/stars/lizardbyte/sunshine.svg?logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine)
 [![GitHub Releases](https://img.shields.io/github/downloads/lizardbyte/sunshine/total.svg?style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/releases/latest)
 [![Docker](https://img.shields.io/docker/pulls/lizardbyte/sunshine.svg?style=for-the-badge&logo=docker)](https://hub.docker.com/r/lizardbyte/sunshine)
+[![Flathub installs](https://img.shields.io/flathub/downloads/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)
+[![Flathub Version](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)
 [![GHCR](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FLizardByte%2FSunshine%2Fsunshine.json&query=%24.downloads&label=ghcr%20pulls&style=for-the-badge&logo=github)](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine)
-[![Winget Version](https://img.shields.io/badge/dynamic/json.svg?color=orange&label=Winget&style=for-the-badge&prefix=v&query=$[-1:].name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fmicrosoft%2Fwinget-pkgs%2Fcontents%2Fmanifests%2Fl%2FLizardByte%2FSunshine&logo=microsoft)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine)
+[![Winget Version](https://img.shields.io/badge/dynamic/json.svg?color=orange&label=Winget&style=for-the-badge&prefix=v&query=$[-1:].name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fmicrosoft%2Fwinget-pkgs%2Fcontents%2Fmanifests%2Fl%2FLizardByte%2FSunshine&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHuSURBVFhH7ZfNTtRQGIYZiMDwN/IrCAqIhMSNKxcmymVwG+5dcDVsWHgDrtxwCYQVl+BChzDEwSnPY+eQ0sxoOz1mQuBNnpyvTdvz9jun5/SrjfxnJUkyQbMEz2ELduF1l0YUA3QyTrMAa2AnPtyOXsELeAYNyKtV2EC3k3lYgTOwg09ghy/BTp7CKBRV844BOpmmMV2+ySb4BmInG7AKY7AHH+EYqqhZo9PPBG/BVDlOizAD/XQFmnoPXzxRQX8M/CCYS48L6RIc4ygGHK9WGg9HZSZMUNRPVwNJGg5Hg2Qgqh4N3FsDsb6EmgYm07iwwvUxstdxJTwgmILf4CfZ6bb5OHANX8GN5x20IVxnG8ge94pt2xpwU3GnCwayF4Q2G2vgFLzHndFzQdk4q77nNfCdwL28qNyMtmEf3A1/QV5FjDiPWo5jrwf8TWZChTlgJvL4F9QL50/A43qVidTvLcuoM2wDQ1+IkgefgUpLcYwMVBqCKNJA2b0gKNocOIITOIef8C/F/CdMbh/GklynsSawKLHS8d9/B1x2LUqsfFyy3TMsWj5A1cLkotDbYO4JjWWZlZEGv8EbOIR1CAVN2eG8W5oNKgxaeC6DmTJjZs7ixUxpznLPLT+v4sXpoMLcLI3mzFSonDXIEI/M3QCIO4YuimBJ/gAAAABJRU5ErkJggg==)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/l/LizardByte/Sunshine)
 [![GitHub Workflow Status (CI)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/CI.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/CI.yml?query=branch%3Amaster)
 [![GitHub Workflow Status (localize)](https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=master&label=localize%20build&logo=github&style=for-the-badge)](https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Amaster)
 [![Read the Docs](https://img.shields.io/readthedocs/sunshinestream.svg?label=Docs&style=for-the-badge&logo=readthedocs)](http://sunshinestream.readthedocs.io)
diff --git a/cmake/compile_definitions/linux.cmake b/cmake/compile_definitions/linux.cmake
index d90f5dc655e..94465abeb38 100644
--- a/cmake/compile_definitions/linux.cmake
+++ b/cmake/compile_definitions/linux.cmake
@@ -227,6 +227,14 @@ else()
     endif()
 endif()
 
+# AppImage and Flatpak
+if (${SUNSHINE_BUILD_APPIMAGE})
+    list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_APPIMAGE=1)
+endif ()
+if (${SUNSHINE_BUILD_FLATPAK})
+    list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_BUILD_FLATPAK=1)
+endif ()
+
 list(APPEND PLATFORM_TARGET_FILES
         "${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp"
         "${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h"
diff --git a/cmake/prep/options.cmake b/cmake/prep/options.cmake
index f1b33f08264..2914c4db882 100644
--- a/cmake/prep/options.cmake
+++ b/cmake/prep/options.cmake
@@ -9,6 +9,7 @@ set(SUNSHINE_PUBLISHER_ISSUE_URL "https://app.lizardbyte.dev/support"
 
 option(BUILD_DOCS "Build documentation" ON)
 option(BUILD_TESTS "Build tests" ON)
+option(NPM_OFFLINE "Use offline npm packages. You must ensure packages are in your npm cache." OFF)
 option(TESTS_ENABLE_PYTHON_TESTS "Enable Python tests" ON)
 
 # DirectX11 is not available in GitHub runners, so even software encoding fails
diff --git a/cmake/prep/special_package_configuration.cmake b/cmake/prep/special_package_configuration.cmake
index 3e5c5d9c370..df1bbb36a6b 100644
--- a/cmake/prep/special_package_configuration.cmake
+++ b/cmake/prep/special_package_configuration.cmake
@@ -40,8 +40,12 @@ elseif(UNIX)
     # configure the flatpak manifest
     if(${SUNSHINE_CONFIGURE_FLATPAK_MAN})
         configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.yml ${PROJECT_FQDN}.yml @ONLY)
+        configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.metainfo.xml
+                ${PROJECT_FQDN}.metainfo.xml @ONLY)
         file(COPY packaging/linux/flatpak/deps/ DESTINATION ${CMAKE_BINARY_DIR})
         file(COPY packaging/linux/flatpak/modules DESTINATION ${CMAKE_BINARY_DIR})
+        file(COPY generated-sources.json DESTINATION ${CMAKE_BINARY_DIR})
+        file(COPY package-lock.json DESTINATION ${CMAKE_BINARY_DIR})
     endif()
 endif()
 
diff --git a/cmake/targets/common.cmake b/cmake/targets/common.cmake
index b086bdd73a4..e928b2c5356 100644
--- a/cmake/targets/common.cmake
+++ b/cmake/targets/common.cmake
@@ -53,10 +53,17 @@ endif()
 
 #WebUI build
 find_program(NPM npm REQUIRED)
+
+if (NPM_OFFLINE)
+    set(NPM_INSTALL_FLAGS "--offline")
+else()
+    set(NPM_INSTALL_FLAGS "")
+endif()
+
 add_custom_target(web-ui ALL
         WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
         COMMENT "Installing NPM Dependencies and Building the Web UI"
-        COMMAND "$<$<BOOL:${WIN32}>:cmd;/C>" "${NPM}" install
+        COMMAND "$<$<BOOL:${WIN32}>:cmd;/C>" "${NPM}" install ${NPM_INSTALL_FLAGS}
         COMMAND "${CMAKE_COMMAND}" -E env "SUNSHINE_BUILD_HOMEBREW=${NPM_BUILD_HOMEBREW}" "SUNSHINE_SOURCE_ASSETS_DIR=${NPM_SOURCE_ASSETS_DIR}" "SUNSHINE_ASSETS_DIR=${NPM_ASSETS_DIR}" "$<$<BOOL:${WIN32}>:cmd;/C>" "${NPM}" run build  # cmake-lint: disable=C0301
         COMMAND_EXPAND_LISTS
         VERBATIM)
diff --git a/docs/getting_started.md b/docs/getting_started.md
index d121813ead2..4604fa561e9 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -53,22 +53,24 @@ CUDA is used for NVFBC capture.
         <td>sunshine-ubuntu-24.04-{arch}.deb</td>
     </tr>
     <tr>
-        <td rowspan="2">12.0.0</td>
-        <td rowspan="4">525.60.13</td>
+        <td rowspan="1">12.0.0</td>
+        <td rowspan="3">525.60.13</td>
         <td rowspan="4">50;52;60;61;62;70;72;75;80;86;87;89;90</td>
-        <td>sunshine_{arch}.flatpak</td>
-    </tr>
-    <tr>
         <td>sunshine-debian-bookworm-{arch}.deb</td>
     </tr>
     <tr>
-        <td>12.4.0</td>
+        <td rowspan="1">12.4.0</td>
         <td>sunshine-fedora-39-{arch}.rpm</td>
     </tr>
     <tr>
-        <td>12.5.1</td>
+        <td rowspan="1">12.5.1</td>
         <td>sunshine.pkg.tar.zst</td>
     </tr>
+    <tr>
+        <td rowspan="1">12.6.2</td>
+        <td rowspan="1">560.35.03</td>
+        <td>sunshine_{arch}.flatpak</td>
+    </tr>
     <tr>
         <td>n/a</td>
         <td>n/a</td>
diff --git a/docs/third_party_packages.md b/docs/third_party_packages.md
index 1793b4ce06b..148672997e1 100644
--- a/docs/third_party_packages.md
+++ b/docs/third_party_packages.md
@@ -5,9 +5,6 @@
 ## Chocolatey
 [![Chocolatey](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=chocolatey&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27chocolatey%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=chocolatey)](https://community.chocolatey.org/packages/sunshine)
 
-## Flathub
-[![Flathub](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=Flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)
-
 ## nixpkgs
 [![nixpkgs](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=nixpkgs&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27nix_unstable%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=nixos)](https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/sunshine/default.nix)
 
diff --git a/package.json b/package.json
index a379c67e6c3..d93d962997b 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,11 @@
 {
+  "name": "sunshine",
+  "version": "0.0.0",
   "scripts": {
     "build": "vite build --debug",
     "build-clean": "vite build --debug --emptyOutDir",
-    "dev": "vite build --watch"
+    "dev": "vite build --watch",
+    "serve": "serve ./tests/fixtures/http --no-port-switching"
   },
   "dependencies": {
     "@lizardbyte/shared-web": "2024.901.195233",
@@ -11,6 +14,7 @@
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "4.6.2",
+    "serve": "14.2.3",
     "vite": "4.5.2",
     "vite-plugin-ejs": "1.6.4"
   }
diff --git a/packaging/linux/flatpak/README.md b/packaging/linux/flatpak/README.md
new file mode 100644
index 00000000000..9b358b79ec3
--- /dev/null
+++ b/packaging/linux/flatpak/README.md
@@ -0,0 +1,13 @@
+# Overview
+
+[![Flathub installs](https://img.shields.io/flathub/downloads/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)
+[![Flathub Version](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)
+
+LizardByte has the full documentation hosted on [Read the Docs](https://sunshinestream.readthedocs.io).
+
+## About
+
+Sunshine is a self-hosted game stream host for Moonlight.
+
+This repo is synced from the upstream [Sunshine](https://github.com/LizardByte/Sunshine) repo.
+Please report issues and contribute to the upstream repo.
diff --git a/packaging/linux/flatpak/deps/flatpak-builder-tools b/packaging/linux/flatpak/deps/flatpak-builder-tools
new file mode 160000
index 00000000000..9a48b5e30a5
--- /dev/null
+++ b/packaging/linux/flatpak/deps/flatpak-builder-tools
@@ -0,0 +1 @@
+Subproject commit 9a48b5e30a53715f1e71a5b804ff99fa46c430a3
diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml
index 8cbbd5a5aa4..7b4f1f18a86 100644
--- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml
+++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.metainfo.xml
@@ -28,26 +28,15 @@
       @PROJECT_LONG_DESCRIPTION@
     </p>
 
-    <p>NOTE: Allow Sunshine Virtual Input (Required)</p>
-    <p>sudo chown $USER /dev/uinput &amp;&amp; echo 'KERNEL=="uinput", SUBSYSTEM=="misc",
-      OPTIONS+="static_node=uinput", TAG+="uaccess"' | sudo tee
-      /etc/udev/rules.d/60-sunshine-input.rules</p>
-    <p>NOTE: Sunshine uses a self-signed certificate. The web browser will report it as not secure,
-      but it is safe.</p>
+    <p>NOTE: Sunshine requires additional installation steps.</p>
+    <p>flatpak run --command=additional-install.sh @PROJECT_FQDN@</p>
+    <p>NOTE: Sunshine uses a self-signed certificate. The web browser will report it as not secure, but it is safe.</p>
     <p>NOTE: KMS Grab (Optional)</p>
-    <p>sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}')
-      flatpak run @PROJECT_FQDN@</p>
+    <p>sudo -i PULSE_SERVER=unix:$(pactl info | awk '/Server String/{print$3}') flatpak run @PROJECT_FQDN@</p>
   </description>
 
   <releases>
-    <release version="0.23.1" date="2024-04-21"/>
-    <release version="0.23.0" date="2024-04-06"/>
-    <release version="0.22.2" date="2024-03-15"/>
-    <release version="0.22.1" date="2024-03-13"/>
-    <release version="0.22.0" date="2024-03-04"/>
-    <release version="0.21.0" date="2023-10-15"/>
-    <release version="0.20.0" date="2023-05-29"/>
-    <release version="0.19.1" date="2023-03-30"/>
+    <release version="@PROJECT_VERSION@" date="1970-01-01"></release>
   </releases>
 
   <developer_name>LizardByte</developer_name>
diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
index 156b40c5dff..3313ed25bc5 100644
--- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
+++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml
@@ -1,11 +1,10 @@
 ---
 app-id: "@PROJECT_FQDN@"
 runtime: org.freedesktop.Platform
-runtime-version: "22.08"
+runtime-version: "23.08"  # requires CUDA >= 12.2
 sdk: org.freedesktop.Sdk
 sdk-extensions:
-  - org.freedesktop.Sdk.Extension.node18
-  - org.freedesktop.Sdk.Extension.vala
+  - org.freedesktop.Sdk.Extension.node20
 command: sunshine
 separate-locales: false
 finish-args:
@@ -29,10 +28,6 @@ cleanup:
   - /lib/*.a
   - /share/man
 
-build-options:
-  append-path: /usr/lib/sdk/vala/bin
-  prepend-ld-library-path: /usr/lib/sdk/vala/lib
-
 modules:
   # Test dependencies
   - "modules/xvfb/xvfb.json"
@@ -40,6 +35,7 @@ modules:
   # Runtime dependencies
   - shared-modules/libappindicator/libappindicator-gtk3-12.10.json
   - "modules/avahi.json"
+  - "modules/boost.json"
   - "modules/libevdev.json"
   - "modules/libnotify.json"
   - "modules/miniupnpc.json"
@@ -49,23 +45,21 @@ modules:
   - "modules/cuda.json"
 
   - name: sunshine
-    disabled: false
-    buildsystem: cmake-ninja
-    no-make-install: false
     builddir: true
     build-options:
-      append-path: /usr/lib/sdk/node18/bin
-      build-args:
-        - --share=network
-      test-args:
-        - --share=network
+      append-path: /usr/lib/sdk/node20/bin
       env:
         BUILD_VERSION: "@BUILD_VERSION@"
         BRANCH: "@GITHUB_BRANCH@"
         COMMIT: "@GITHUB_COMMIT@"
-        npm_config_nodedir: /usr/lib/sdk/node18
+        XDG_CACHE_HOME: /run/build/sunshine/flatpak-node/cache
+        npm_config_cache: /run/build/sunshine/flatpak-node/npm-cache
+        npm_config_nodedir: /usr/lib/sdk/node20
+        npm_config_offline: 'true'
         NPM_CONFIG_LOGLEVEL: info
+    buildsystem: cmake-ninja
     config-opts:
+      - -DBOOST_USE_STATIC=OFF
       - -DBUILD_DOCS=OFF
       - -DBUILD_WERROR=ON
       - -DCMAKE_BUILD_TYPE=Release
@@ -78,16 +72,20 @@ modules:
       - -DSUNSHINE_ENABLE_DRM=ON
       - -DSUNSHINE_ENABLE_CUDA=ON
       - -DSUNSHINE_PUBLISHER_NAME='LizardByte'
-        -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev'
-        -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support'
-    sources:
-      - type: git
-        url: "@GITHUB_CLONE_URL@"
-        commit: "@GITHUB_COMMIT@"
+      - -DSUNSHINE_PUBLISHER_WEBSITE='https://app.lizardbyte.dev'
+      - -DSUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support'
+    no-make-install: false
     post-install:
       - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/scripts/* /app/bin
       - install -D $FLATPAK_BUILDER_BUILDDIR/packaging/linux/flatpak/apps.json /app/share/sunshine/apps.json
     run-tests: true
     test-rule: ""  # empty to disable
     test-commands:
-      - xvfb-run tests/test_sunshine --gtest_color=yes
+      - npm run serve & xvfb-run tests/test_sunshine --gtest_color=yes
+    sources:
+      - generated-sources.json
+      - type: git
+        url: "@GITHUB_CLONE_URL@"
+        commit: "@GITHUB_COMMIT@"
+      - type: file
+        path: package-lock.json
diff --git a/packaging/linux/flatpak/flathub.json b/packaging/linux/flatpak/flathub.json
new file mode 100644
index 00000000000..2de28147bbb
--- /dev/null
+++ b/packaging/linux/flatpak/flathub.json
@@ -0,0 +1,3 @@
+{
+  "disable-external-data-checker": true
+}
diff --git a/packaging/linux/flatpak/modules/boost.json b/packaging/linux/flatpak/modules/boost.json
new file mode 100644
index 00000000000..da111f64816
--- /dev/null
+++ b/packaging/linux/flatpak/modules/boost.json
@@ -0,0 +1,16 @@
+{
+  "name": "boost",
+  "buildsystem": "simple",
+  "build-commands": [
+    "cd tools/build && bison -y -d -o src/engine/jamgram.cpp src/engine/jamgram.y",
+    "./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=filesystem,locale,log,program_options,system",
+    "./b2 install variant=release link=shared runtime-link=shared cxxflags=\"$CXXFLAGS\""
+  ],
+  "sources": [
+    {
+      "type": "archive",
+      "url": "https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-cmake.tar.xz",
+      "sha256": "2c5ec5edcdff47ff55e27ed9560b0a0b94b07bd07ed9928b476150e16b0efc57"
+    }
+  ]
+}
diff --git a/packaging/linux/flatpak/modules/cuda.json b/packaging/linux/flatpak/modules/cuda.json
index 53a38c0c69f..3e10f2fb7df 100644
--- a/packaging/linux/flatpak/modules/cuda.json
+++ b/packaging/linux/flatpak/modules/cuda.json
@@ -19,8 +19,8 @@
       "only-arches": [
         "x86_64"
       ],
-      "url": "https://developer.download.nvidia.com/compute/cuda/12.0.0/local_installers/cuda_12.0.0_525.60.13_linux.run",
-      "sha256": "905e9b9516900839fb76064719db752439f38b8cb730b49335d8bd53ddfad392",
+      "url": "https://developer.download.nvidia.com/compute/cuda/12.6.2/local_installers/cuda_12.6.2_560.35.03_linux.run",
+      "sha256": "3729a89cb58f7ca6a46719cff110d6292aec7577585a8d71340f0dbac54fb237",
       "dest-filename": "cuda.run"
     },
     {
@@ -28,8 +28,8 @@
       "only-arches": [
         "aarch64"
       ],
-      "url": "https://developer.download.nvidia.com/compute/cuda/12.0.0/local_installers/cuda_12.0.0_525.60.13_linux_sbsa.run",
-      "sha256": "cd13e9c65d4c8f895a968706f46064d536be09f9706bce081cc864b7e4fa4544",
+      "url": "https://developer.download.nvidia.com/compute/cuda/12.6.2/local_installers/cuda_12.6.2_560.35.03_linux_sbsa.run",
+      "sha256": "2249408848b705c18b9eadfb5161b52e4e36fcc5753647329cce93db141e5466",
       "dest-filename": "cuda.run"
     }
   ]
diff --git a/tests/fixtures/http/hello-redirect.txt b/tests/fixtures/http/hello-redirect.txt
new file mode 100644
index 00000000000..64a358d994a
--- /dev/null
+++ b/tests/fixtures/http/hello-redirect.txt
@@ -0,0 +1 @@
+hello-redirect.txt
diff --git a/tests/fixtures/http/hello.txt b/tests/fixtures/http/hello.txt
new file mode 100644
index 00000000000..f0ad0dec66b
--- /dev/null
+++ b/tests/fixtures/http/hello.txt
@@ -0,0 +1 @@
+hello.txt
diff --git a/tests/unit/test_httpcommon.cpp b/tests/unit/test_httpcommon.cpp
index bb0bb5625f7..3b2b37e3869 100644
--- a/tests/unit/test_httpcommon.cpp
+++ b/tests/unit/test_httpcommon.cpp
@@ -45,9 +45,18 @@ TEST_P(DownloadFileTest, Run) {
   ASSERT_TRUE(http::download_file(url, path));
 }
 
+#ifdef SUNSHINE_BUILD_FLATPAK
+// requires running `npm run serve` prior to running the tests
+constexpr const char *URL_1 = "http://0.0.0.0:3000/hello.txt";
+constexpr const char *URL_2 = "http://0.0.0.0:3000/hello-redirect.txt";
+#else
+constexpr const char *URL_1 = "https://httpbin.org/base64/aGVsbG8h";
+constexpr const char *URL_2 = "https://httpbin.org/redirect-to?url=/base64/aGVsbG8h";
+#endif
+
 INSTANTIATE_TEST_SUITE_P(
   DownloadFileTests,
   DownloadFileTest,
   testing::Values(
-    std::make_tuple("https://httpbin.org/base64/aGVsbG8h", "hello.txt"),
-    std::make_tuple("https://httpbin.org/redirect-to?url=/base64/aGVsbG8h", "hello-redirect.txt")));
+    std::make_tuple(URL_1, "hello.txt"),
+    std::make_tuple(URL_2, "hello-redirect.txt")));