diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 00fb5d7283f3..c12ef3636066 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -6,6 +6,10 @@ inputs:
     description: "Indicates if node is set up for hybrid app"
     required: false
     default: 'false'
+  IS_DESKTOP_BUILD:
+    description: "Indicates if node is set up for desktop app"
+    required: false
+    default: 'false'
 
 outputs:
   cache-hit:
@@ -23,7 +27,9 @@ runs:
       with:
         node-version-file: '.nvmrc'
         cache: npm
-        cache-dependency-path: normalized-package-lock.json
+        cache-dependency-path: |
+          normalized-package-lock.json
+          desktop/package-lock.json
 
     - id: cache-node-modules
       # v4
@@ -40,6 +46,14 @@ runs:
         path: Mobile-Expensify/node_modules
         key: ${{ runner.os }}-node-modules-${{ hashFiles('Mobile-Expensify/package-lock.json', 'Mobile-Expensify/patches/**') }}
 
+    - id: cache-desktop-node-modules
+      if: inputs.IS_DESKTOP_BUILD == 'true'
+      # v4
+      uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57
+      with:
+        path: desktop/node_modules
+        key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json', 'desktop/patches/**') }}
+
     - name: Remove ND node_modules if needed for hybrid app build
       if: inputs.IS_HYBRID_BUILD == 'true' && steps.cache-node-modules.outputs.cache-hit == 'true' && steps.cache-old-dot-node-modules.outputs.cache-hit != 'true'
       shell: bash
@@ -51,4 +65,12 @@ runs:
       with:
         timeout_minutes: 30
         max_attempts: 3
-        command: npm ci
\ No newline at end of file
+        command: npm ci
+
+    - name: Install node packages for desktop submodule
+      if: inputs.IS_DESKTOP_BUILD == 'true' && steps.cache-desktop-node-modules.outputs.cache-hit != 'true'
+      uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
+      with:
+        timeout_minutes: 30
+        max_attempts: 3
+        command: cd desktop && npm ci
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
index b8a39af6c3ae..f7af5e586968 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml
@@ -20,6 +20,9 @@ inputs:
     ANDROID:
         description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')"
         required: true
+    DESKTOP:
+        description: "Desktop job result ('success', 'failure', 'cancelled', or 'skipped')"
+        required: true
     IOS:
         description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')"
         required: true
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
index a13c54e6b1ae..d8d7a926f00b 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js
@@ -12840,6 +12840,7 @@ async function run() {
     const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true });
     const version = core.getInput('DEPLOY_VERSION', { required: true });
     const androidResult = getDeployTableMessage(core.getInput('ANDROID', { required: true }));
+    const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true }));
     const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true }));
     const webResult = getDeployTableMessage(core.getInput('WEB', { required: true }));
     const date = core.getInput('DATE');
@@ -12851,7 +12852,7 @@ async function run() {
             message += `on ${date}`;
         }
         message += `🚀`;
-        message += `\n\nplatform | result\n---|---`;
+        message += `\n\nplatform | result\n---|---\n🖥 desktop 🖥|${desktopResult}`;
         message += `\n🕸 web 🕸|${webResult}`;
         message += `\n🤖 android 🤖|${androidResult}\n🍎 iOS 🍎|${iOSResult}`;
         if (note) {
diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
index bf7ce3c35f44..93131cb8125e 100644
--- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
+++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts
@@ -103,6 +103,7 @@ async function run() {
     const version = core.getInput('DEPLOY_VERSION', {required: true});
 
     const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true}) as PlatformResult);
+    const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true}) as PlatformResult);
     const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult);
     const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult);
 
@@ -116,7 +117,7 @@ async function run() {
             message += `on ${date}`;
         }
         message += `🚀`;
-        message += `\n\nplatform | result\n---|---`;
+        message += `\n\nplatform | result\n---|---\n🖥 desktop 🖥|${desktopResult}`;
         message += `\n🕸 web 🕸|${webResult}`;
         message += `\n🤖 android 🤖|${androidResult}\n🍎 iOS 🍎|${iOSResult}`;
 
diff --git a/.github/actions/javascript/postTestBuildComment/action.yml b/.github/actions/javascript/postTestBuildComment/action.yml
index ea2c75475f9e..ad97aa9015f4 100644
--- a/.github/actions/javascript/postTestBuildComment/action.yml
+++ b/.github/actions/javascript/postTestBuildComment/action.yml
@@ -16,6 +16,9 @@ inputs:
     ANDROID:
         description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')"
         required: false
+    DESKTOP:
+        description: "Desktop job result ('success', 'failure', 'cancelled', or 'skipped')"
+        required: false
     IOS:
         description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')"
         required: false
@@ -25,6 +28,9 @@ inputs:
     ANDROID_LINK:
         description: "Link for the Android build"
         required: false
+    DESKTOP_LINK:
+        description: "Link for the desktop build"
+        required: false
     IOS_LINK:
         description: "Link for the iOS build"
         required: false
diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js
index 3a31cd257a72..eb1ade0901c4 100644
--- a/.github/actions/javascript/postTestBuildComment/index.js
+++ b/.github/actions/javascript/postTestBuildComment/index.js
@@ -11585,11 +11585,12 @@ const github_1 = __nccwpck_require__(5438);
 const CONST_1 = __importDefault(__nccwpck_require__(9873));
 const GithubUtils_1 = __importDefault(__nccwpck_require__(9296));
 function getTestBuildMessage(appPr, mobileExpensifyPr) {
-    const inputs = ['ANDROID', 'IOS', 'WEB'];
+    const inputs = ['ANDROID', 'DESKTOP', 'IOS', 'WEB'];
     const names = {
         [inputs[0]]: 'Android',
-        [inputs[1]]: 'iOS',
-        [inputs[2]]: 'Web',
+        [inputs[1]]: 'Desktop',
+        [inputs[2]]: 'iOS',
+        [inputs[3]]: 'Web',
     };
     const result = inputs.reduce((acc, platform) => {
         const input = core.getInput(platform, { required: false });
@@ -11618,17 +11619,17 @@ function getTestBuildMessage(appPr, mobileExpensifyPr) {
         };
         return acc;
     }, {});
-    const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS${appPr ? ', and Web' : ''}. Happy testing! :test_tube::test_tube:
+    const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS${appPr ? ', Desktop, and Web' : ''}. Happy testing! :test_tube::test_tube:
 Built from${appPr ? ` App PR Expensify/App#${appPr}` : ''}${mobileExpensifyPr ? ` Mobile-Expensify PR Expensify/Mobile-Expensify#${mobileExpensifyPr}` : ''}.
 | Android :robot:  | iOS :apple: |
 | ------------- | ------------- |
 | ${result.ANDROID.link}  | ${result.IOS.link}  |
 | ${result.ANDROID.qrCode}  | ${result.IOS.qrCode}  |
 
-| Web :spider_web: |
-| ------------- |
-| ${result.WEB.link}  |
-| ${result.WEB.qrCode}  |
+| Desktop :computer: | Web :spider_web: |
+| ------------- | ------------- |
+| ${result.DESKTOP.link}  | ${result.WEB.link}  |
+| ${result.DESKTOP.qrCode}  | ${result.WEB.qrCode}  |
 
 ---
 
diff --git a/.github/actions/javascript/postTestBuildComment/postTestBuildComment.ts b/.github/actions/javascript/postTestBuildComment/postTestBuildComment.ts
index a2278e10fce5..8a825cf8e09c 100644
--- a/.github/actions/javascript/postTestBuildComment/postTestBuildComment.ts
+++ b/.github/actions/javascript/postTestBuildComment/postTestBuildComment.ts
@@ -5,11 +5,12 @@ import CONST from '@github/libs/CONST';
 import GithubUtils from '@github/libs/GithubUtils';
 
 function getTestBuildMessage(appPr?: number, mobileExpensifyPr?: number): string {
-    const inputs = ['ANDROID', 'IOS', 'WEB'] as const;
+    const inputs = ['ANDROID', 'DESKTOP', 'IOS', 'WEB'] as const;
     const names = {
         [inputs[0]]: 'Android',
-        [inputs[1]]: 'iOS',
-        [inputs[2]]: 'Web',
+        [inputs[1]]: 'Desktop',
+        [inputs[2]]: 'iOS',
+        [inputs[3]]: 'Web',
     };
 
     const result = inputs.reduce(
@@ -46,17 +47,17 @@ function getTestBuildMessage(appPr?: number, mobileExpensifyPr?: number): string
         {} as Record<TupleToUnion<typeof inputs>, {link: string; qrCode: string}>,
     );
 
-    const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS${appPr ? ', and Web' : ''}. Happy testing! :test_tube::test_tube:
+    const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS${appPr ? ', Desktop, and Web' : ''}. Happy testing! :test_tube::test_tube:
 Built from${appPr ? ` App PR Expensify/App#${appPr}` : ''}${mobileExpensifyPr ? ` Mobile-Expensify PR Expensify/Mobile-Expensify#${mobileExpensifyPr}` : ''}.
 | Android :robot:  | iOS :apple: |
 | ------------- | ------------- |
 | ${result.ANDROID.link}  | ${result.IOS.link}  |
 | ${result.ANDROID.qrCode}  | ${result.IOS.qrCode}  |
 
-| Web :spider_web: |
-| ------------- |
-| ${result.WEB.link}  |
-| ${result.WEB.qrCode}  |
+| Desktop :computer: | Web :spider_web: |
+| ------------- | ------------- |
+| ${result.DESKTOP.link}  | ${result.WEB.link}  |
+| ${result.DESKTOP.qrCode}  | ${result.WEB.qrCode}  |
 
 ---
 
diff --git a/.github/workflows/checkE2ETestCode.yml b/.github/workflows/checkE2ETestCode.yml
index 25c2be16c28b..60ca53e2699f 100644
--- a/.github/workflows/checkE2ETestCode.yml
+++ b/.github/workflows/checkE2ETestCode.yml
@@ -19,6 +19,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: 'true'
 
       - name: Verify e2e tests compile correctly
         run: npm run e2e-test-runner-build
diff --git a/.github/workflows/checkSVGCompression.yml b/.github/workflows/checkSVGCompression.yml
index 534c5aaf367d..5947a04a454d 100644
--- a/.github/workflows/checkSVGCompression.yml
+++ b/.github/workflows/checkSVGCompression.yml
@@ -26,6 +26,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
 
       - name: Compress SVG check
         uses: ./.github/actions/javascript/checkSVGCompression
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 875e8fabdc2c..7444ef53f006 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -287,6 +287,70 @@ jobs:
           GITHUB_TOKEN: ${{ github.token }}
           SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
 
+  desktop:
+    name: Build and deploy Desktop
+    needs: prep
+    runs-on: macos-14-large
+    steps:
+      - name: Checkout
+        # v4
+        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
+
+      - name: Setup Node
+        uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
+
+        # v6.0.0
+      - name: Setup Python for node-gyp
+        id: setup-python
+        uses: actions/setup-python@18566f86b301499665bd3eb1a2247e0849c64fa5
+        with:
+          python-version: '3.12'
+          cache: 'pip'
+          cache-dependency-path: desktop/requirements.txt
+
+      - name: Ensure setuptools for node-gyp
+        run: pip install --upgrade -r desktop/requirements.txt
+
+      - name: Load Desktop credentials from 1Password
+        id: load-credentials
+        # v2
+        uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0
+        with:
+          export-env: false
+        env:
+          OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+          DESKTOP_CERTIFICATE_BASE64: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_LINK"
+          DESKTOP_CERTIFICATE_PASSWORD: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_KEY_PASSWORD"
+
+      - name: Build desktop app
+        run: ${{ github.ref == 'refs/heads/production' && 'npm run desktop-build' || 'npm run desktop-build-staging' }}
+        env:
+          CSC_LINK: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_BASE64 }}
+          CSC_KEY_PASSWORD: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_PASSWORD }}
+          APPLE_ID: ${{ secrets.APPLE_ID }}
+          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }}
+          S3_BUCKET: ${{ github.ref == 'refs/heads/production' && vars.PRODUCTION_S3_BUCKET || vars.STAGING_S3_BUCKET }}
+
+      - name: Upload desktop sourcemaps artifact
+        # v4
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+        with:
+          name: desktop-sourcemaps-artifact
+          path: ./desktop/dist/www/merged-source-map.js.map
+
+      - name: Upload desktop build artifact
+        # v4
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+        with:
+          name: desktop-build-artifact
+          path: ./desktop-build/NewExpensify.dmg
+
   ios:
     name: Build and deploy iOS HybridApp
     needs: prep
@@ -551,7 +615,7 @@ jobs:
     name: Post a Slack message when any platform fails to build or deploy
     runs-on: ubuntu-latest
     if: ${{ failure() }}
-    needs: [android, ios, web]
+    needs: [android, desktop, ios, web]
     steps:
       - name: Checkout
         # v4
@@ -567,7 +631,7 @@ jobs:
     outputs:
       IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }}
       IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }}
-    needs: [android, ios, web]
+    needs: [android, desktop, ios, web]
     if: ${{ always() }}
     steps:
       - name: Check deployment success on at least one platform
@@ -576,6 +640,7 @@ jobs:
           isAtLeastOnePlatformDeployed="false"
           if [ "${{ needs.iOS.result }}" == "success" ] || \
             [ "${{ needs.android.result }}" == "success" ] || \
+            [ "${{ needs.desktop.result }}" == "success" ] || \
             [ "${{ needs.web.result }}" == "success" ]; then
             isAtLeastOnePlatformDeployed="true"
           fi
@@ -588,6 +653,7 @@ jobs:
           isAllPlatformsDeployed="false"
           if [ "${{ needs.iOS.result }}" == "success" ] && \
             [ "${{ needs.android.result }}" == "success" ] && \
+            [ "${{ needs.desktop.result }}" == "success" ] && \
             [ "${{ needs.web.result }}" == "success" ]; then
             isAllPlatformsDeployed="true"
           fi
@@ -635,9 +701,10 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ github.token }}
 
-      - name: Rename web sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
+      - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
         continue-on-error: true
         run: |
+          mv ./desktop-sourcemaps-artifact/merged-source-map.js.map ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map
           mv ./web-sourcemaps-artifact/merged-source-map.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map
 
       - name: Upload artifacts to GitHub Release
@@ -648,6 +715,8 @@ jobs:
             ./android-build-artifact/Expensify-release.aab#android.aab
             ./android-apk-artifact/Expensify.apk#android.apk
             ./android-sourcemap-artifact/index.android.bundle.map#android-sourcemap.js.map
+            ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-sourcemap.js.map
+            ./desktop-build-artifact/NewExpensify.dmg#desktop.dmg
             ./ios-build-artifact/Expensify.ipa#ios.ipa
             ./ios-sourcemap-artifact/main.jsbundle.map#ios-sourcemap.js.map
             ./web-sourcemaps-artifact/web-merged-source-map.js.map#web-sourcemap.js.map
@@ -707,7 +776,7 @@ jobs:
     name: Post a Slack message when all platforms deploy successfully
     runs-on: ubuntu-latest
     if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }}
-    needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease]
+    needs: [prep, android, desktop, ios, web, checkDeploymentSuccess, createRelease]
     steps:
       - name: 'Announces the deploy in the #announce Slack room'
         # v3
@@ -764,7 +833,7 @@ jobs:
   postGithubComments:
     uses: ./.github/workflows/postDeployComments.yml
     if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
-    needs: [prep, android, ios, web, checkDeploymentSuccess, createRelease]
+    needs: [prep, android, desktop, ios, web, checkDeploymentSuccess, createRelease]
     secrets: inherit
     with:
       version: ${{ needs.prep.outputs.APP_VERSION }}
@@ -772,3 +841,4 @@ jobs:
       android: ${{ needs.android.result }}
       ios: ${{ needs.ios.result }}
       web: ${{ needs.web.result }}
+      desktop: ${{ needs.desktop.result }}
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index 822deea3edd6..de82e002c96b 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -113,6 +113,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: 'true'
 
       - name: Make zip directory for everything to send to AWS Device Farm
         run: mkdir zip
diff --git a/.github/workflows/lint-changed.yml b/.github/workflows/lint-changed.yml
index 00f05209cfa7..7bba2072bdd6 100644
--- a/.github/workflows/lint-changed.yml
+++ b/.github/workflows/lint-changed.yml
@@ -54,6 +54,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
  
       - name: Run ESLint to check for deprecation warnings
         run: |
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index baa630c61ef8..fe3855db4b54 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,6 +23,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
 
       - name: Lint JavaScript and Typescript with ESLint
         run: npm run lint
diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml
index 35123e3d1504..1b653085c114 100644
--- a/.github/workflows/postDeployComments.yml
+++ b/.github/workflows/postDeployComments.yml
@@ -23,6 +23,10 @@ on:
         description: Web deploy status
         required: true
         type: string
+      desktop:
+        description: Desktop deploy status
+        required: true
+        type: string
     secrets:
       OS_BOTIFY_TOKEN:
         description: Token for accessing Mobile-Expensify repository
@@ -67,6 +71,15 @@ on:
           - failure
           - cancelled
           - skipped
+      desktop:
+        description: Desktop deploy status
+        required: true
+        type: choice
+        options:
+          - success
+          - failure
+          - cancelled
+          - skipped
       date:
         description: The date when this deploy occurred
         required: false
@@ -105,6 +118,7 @@ jobs:
           DEPLOY_VERSION: ${{ inputs.version }}
           GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
           ANDROID: ${{ inputs.android }}
+          DESKTOP: ${{ inputs.desktop }}
           IOS: ${{ inputs.ios }}
           WEB: ${{ inputs.web }}
           DATE: ${{ inputs.date }}
diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml
index 12af5722e60e..5b68e033ee66 100644
--- a/.github/workflows/prettier.yml
+++ b/.github/workflows/prettier.yml
@@ -23,6 +23,8 @@ jobs:
 
       - name: Setup Node
         uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
 
       - name: Verify there's no Prettier diff
         run: |
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 73b4dbe059b2..3f8a83213502 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -22,6 +22,10 @@ on:
         description: Should build web app?
         type: boolean
         default: true
+      DESKTOP:
+        description: Should build desktop app?
+        type: boolean
+        default: true
       IOS:
         description: Should build iOS app?
         type: boolean
@@ -158,6 +162,73 @@ jobs:
               body: `🚧 @${{ github.actor }} has triggered a test Expensify/Mobile-Expensify build. You can view the [workflow run here](${workflowURL}).`
               });
 
+  desktop:
+    name: Build and deploy Desktop for testing
+    if: ${{ inputs.DESKTOP && inputs.APP_PULL_REQUEST_NUMBER }}
+    needs: [prep]
+    runs-on: macos-14-large
+    steps:
+      - name: Checkout
+        # v4
+        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
+        with:
+          ref: ${{ needs.prep.outputs.APP_REF }}
+
+      - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it
+        run: |
+          cp .env.staging .env.adhoc
+          sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc
+          echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc
+
+      - name: Setup Node
+        uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
+
+      # v6.0.0
+      - name: Setup python for node-gyp
+        id: setup-python
+        uses: actions/setup-python@18566f86b301499665bd3eb1a2247e0849c64fa5
+        with:
+          python-version: '3.12'
+          cache: 'pip'
+          cache-dependency-path: desktop/requirements.txt
+
+      - name: Ensure setuptools for node-gyp
+        run: pip install --upgrade -r desktop/requirements.txt
+
+      - name: Load Desktop credentials from 1Password
+        id: load-credentials
+        # v2
+        uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0
+        with:
+          export-env: false
+        env:
+          OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
+          DESKTOP_CERTIFICATE_BASE64: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_LINK"
+          DESKTOP_CERTIFICATE_PASSWORD: "op://${{ vars.OP_VAULT }}/Desktop Certificates.p12/CSC_KEY_PASSWORD"
+
+      - name: Configure AWS Credentials
+        # v4
+        uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-1
+
+      - name: Build desktop app for testing
+        run: npm run desktop-build-adhoc
+        env:
+          CSC_LINK: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_BASE64 }}
+          CSC_KEY_PASSWORD: ${{ steps.load-credentials.outputs.DESKTOP_CERTIFICATE_PASSWORD }}
+          APPLE_ID: ${{ secrets.APPLE_ID }}
+          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
+          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }}
+          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+
   web:
     name: Build and deploy Web
     if: ${{ inputs.WEB && inputs.APP_PULL_REQUEST_NUMBER }}
@@ -427,7 +498,7 @@ jobs:
     runs-on: ubuntu-latest
     if: always()
     name: Post a GitHub comment with app download links for testing
-    needs: [prep, getMobileExpensifyPR, web, androidHybrid, iosHybrid]
+    needs: [prep, getMobileExpensifyPR, desktop, web, androidHybrid, iosHybrid]
     steps:
       - name: Checkout
         # v4
@@ -448,9 +519,11 @@ jobs:
           MOBILE_EXPENSIFY_PR_NUMBER: ${{ needs.getMobileExpensifyPR.outputs.MOBILE_EXPENSIFY_PR }}
           GITHUB_TOKEN: ${{ github.token }}
           ANDROID: ${{ needs.androidHybrid.result }}
+          DESKTOP: ${{ needs.desktop.result }}
           IOS: ${{ needs.iosHybrid.result }}
           WEB: ${{ needs.web.result }}
           ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }}
+          DESKTOP_LINK: https://ad-hoc-expensify-cash.s3.amazonaws.com/desktop/${{ inputs.APP_PULL_REQUEST_NUMBER }}/NewExpensify.dmg
           IOS_LINK: ${{ needs.iosHybrid.outputs.IOS_PATH }}
           WEB_LINK: https://${{ inputs.APP_PULL_REQUEST_NUMBER }}.pr-testing.expensify.com
 
diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml
index 6d48f9a933c8..c675f3a3b5a2 100644
--- a/.github/workflows/typecheck.yml
+++ b/.github/workflows/typecheck.yml
@@ -19,6 +19,8 @@ jobs:
       - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
 
       - uses: ./.github/actions/composite/setupNode
+        with:
+          IS_DESKTOP_BUILD: true
 
       - name: Type check with TypeScript
         run: npm run typecheck
diff --git a/contributingGuides/SETUP_WEB.md b/contributingGuides/SETUP_WEB.md
index 533af3b30741..db25bc371d23 100644
--- a/contributingGuides/SETUP_WEB.md
+++ b/contributingGuides/SETUP_WEB.md
@@ -55,7 +55,7 @@ Creating an `.env` file is not necessary. We advise external contributors agains
 - `EXPENSIFY_PARTNER_NAME` - Constant used for the app when authenticating.
 - `EXPENSIFY_PARTNER_PASSWORD` - Another constant used for the app when authenticating. (This is OK to be public)
 - `PUSHER_APP_KEY` - Key used to authenticate with Pusher.com
-- `USE_WEB_PROXY`⚠️- Used in web development, it starts a server along the local development server to proxy requests to the backend. External contributors should set this to `true` otherwise they'll have CORS errors. If you don't want to start the proxy server set this explicitly to `false`
+- `USE_WEB_PROXY`⚠️- Used in web/desktop development, it starts a server along the local development server to proxy requests to the backend. External contributors should set this to `true` otherwise they'll have CORS errors. If you don't want to start the proxy server set this explicitly to `false`
 
 ### Optional Performance Variables
 - `CAPTURE_METRICS` (optional) - Set this to `true` to capture performance metrics and see them in Flipper. See [PERFORMANCE.md](contributingGuides/PERFORMANCE.md#performance-metrics-opt-in-on-local-release-builds) for more information
diff --git a/contributingGuides/philosophies/DEPLOYING.md b/contributingGuides/philosophies/DEPLOYING.md
index 69f6c0fb6c4b..584ed5fdecef 100644
--- a/contributingGuides/philosophies/DEPLOYING.md
+++ b/contributingGuides/philosophies/DEPLOYING.md
@@ -53,7 +53,7 @@ The [`deploy` workflow](https://github.com/Expensify/App/blob/main/.github/workf
 - If `production` was updated, it creates a GitHub Release for the new version.
 
 ### platformDeploy
-The [`platformDeploy` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/platformDeploy.yml) is what actually runs the deployment on all three platforms (iOS, Android, Web). It runs a staging deploy whenever a new tag is pushed to GitHub, and runs a production deploy whenever a new release is created.
+The [`platformDeploy` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/platformDeploy.yml) is what actually runs the deployment on all four platforms (iOS, Android, Web, macOS Desktop). It runs a staging deploy whenever a new tag is pushed to GitHub, and runs a production deploy whenever a new release is created.
 
 ### lockDeploys
 The [`lockDeploys` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/lockDeploys.yml) executes when the `StagingDeployCash` is locked, and it waits for any currently running staging deploys to finish, then gives Applause the :green_circle: to begin QA by commenting in the `StagingDeployCash` checklist.
@@ -62,7 +62,7 @@ The [`lockDeploys` workflow](https://github.com/Expensify/App/blob/main/.github/
 The [`finishReleaseCycle` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/finishReleaseCycle.yml) executes when the `StagingDeployCash` is closed. It updates the `production` branch from `staging` (triggering a production deploy), deploys `main` to staging (with a new `PATCH` version), and creates a new `StagingDeployCash` deploy checklist.
 
 ### testBuild
-The [`testBuild` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/testBuild.yml) builds ad-hoc staging apps (hybrid iOS, hybrid Android, and web) from pull requests submitted to the App and Mobile-Expensify repositories. This process enables testers to review modifications before they are merged into the main branch and deployed to the staging environment. This workflow accepts up to two inputs:
+The [`testBuild` workflow](https://github.com/Expensify/App/blob/main/.github/workflows/testBuild.yml) builds ad-hoc staging apps (hybrid iOS, hybrid Android, web, and desktop) from pull requests submitted to the App and Mobile-Expensify repositories. This process enables testers to review modifications before they are merged into the main branch and deployed to the staging environment. This workflow accepts up to two inputs:
 - A PR number from the App repository for testing New Dot (ND) changes.
 - A PR number from the Mobile-Expensify repository for testing Old Dot (OD) changes.
 
@@ -75,6 +75,26 @@ Sometimes it might be beneficial to generate a local production version instead
 ### - Web app local production builds SHOULD use `npm run build`
 In order to generate a production web build, run `npm run build`, this will generate a production javascript build in the `dist/` folder.
 
+### - MacOS desktop app local production builds SHOULD disable publishing for local testing
+The commands used to compile a production or staging desktop build are `npm run desktop-build` and `npm run desktop-build-staging`, respectively. These will produce an app in the `dist/Mac` folder named NewExpensify.dmg that you can install like a normal app.
+
+HOWEVER, by default those commands will try to notarize the build (signing it as Expensify) and publish it to the S3 bucket where it's hosted for users. In most cases you won't actually need or want to do that for your local testing. To get around that and disable those behaviors for your local build, apply the following diff:
+
+```diff
+diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh
+index 791f59d733..526306eec1 100755
+--- a/scripts/build-desktop.sh
++++ b/scripts/build-desktop.sh
+@@ -35,4 +35,4 @@ npx webpack --config config/webpack/webpack.desktop.ts --env file=$ENV_FILE
+ title "Building Desktop App Archive Using Electron"
+ info ""
+ shift 1
+-npx electron-builder --config config/electronBuilder.config.js --publish always "$@"
++npx electron-builder --config config/electronBuilder.config.js --publish never "$@"
+```
+
+There may be some cases where you need to test a signed and published build, such as when testing the update flows. Instructions on setting that up can be found in [Testing Electron Auto-Update](https://github.com/Expensify/App/blob/main/desktop/README.md#testing-electron-auto-update). Good luck 🙃
+
 ### - iOS app local production builds SHOULD use `npm run ios-build`
 In order to compile a production iOS build, run `npm run ios-build`, this will generate a `Chat.ipa` in the root directory of this project.
 
diff --git a/eslint.config.mjs b/eslint.config.mjs
index cf4b5ce55c10..a980f1107434 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -598,7 +598,6 @@ const config = defineConfig([
         '**/vendor',
         'modules/group-ib-fp/**/*',
         'web/snippets/gib.js',
-        'desktop/**/*',
     ]),
 ]);
 
diff --git a/package.json b/package.json
index c48e6ad15fc8..a455eee698d5 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,12 @@
     "build": "webpack --config config/webpack/webpack.common.ts --env file=.env.production && ts-node ./scripts/combine-web-sourcemaps.ts",
     "build-staging": "webpack --config config/webpack/webpack.common.ts --env file=.env.staging && ts-node ./scripts/combine-web-sourcemaps.ts",
     "build-adhoc": "webpack --config config/webpack/webpack.common.ts --env file=.env.adhoc && ts-node ./scripts/combine-web-sourcemaps.ts",
+    "desktop": "./scripts/set-pusher-suffix.sh && ts-node desktop/start.ts",
+    "desktop-build": "./scripts/build-desktop.sh production",
+    "desktop-build-staging": "./scripts/build-desktop.sh staging",
     "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.ts",
     "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts",
+    "desktop-build-adhoc": "./scripts/build-desktop.sh adhoc",
     "ios-build": "bundle exec fastlane ios build_unsigned",
     "ios-hybrid-build": "bundle exec fastlane ios build_unsigned_hybrid",
     "android-build": "bundle exec fastlane android build_local",
diff --git a/tests/unit/markPullRequestsAsDeployedTest.ts b/tests/unit/markPullRequestsAsDeployedTest.ts
index 2f19afc2cdfc..fb29e697f1e3 100644
--- a/tests/unit/markPullRequestsAsDeployedTest.ts
+++ b/tests/unit/markPullRequestsAsDeployedTest.ts
@@ -81,6 +81,7 @@ function mockGetInputDefaultImplementation(key: string): boolean | string {
             return version;
         case 'IOS':
         case 'ANDROID':
+        case 'DESKTOP':
         case 'WEB':
             return 'success';
         case 'DATE':
@@ -191,6 +192,7 @@ describe('markPullRequestsAsDeployed', () => {
 
 platform | result
 ---|---
+🖥 desktop 🖥|success ✅
 🕸 web 🕸|success ✅
 🤖 android 🤖|success ✅
 🍎 iOS 🍎|success ✅`,
@@ -220,6 +222,7 @@ platform | result
 
 platform | result
 ---|---
+🖥 desktop 🖥|success ✅
 🕸 web 🕸|success ✅
 🤖 android 🤖|success ✅
 🍎 iOS 🍎|success ✅`,
@@ -278,6 +281,7 @@ platform | result
 
 platform | result
 ---|---
+🖥 desktop 🖥|success ✅
 🕸 web 🕸|success ✅
 🤖 android 🤖|success ✅
 🍎 iOS 🍎|success ✅`,
@@ -295,6 +299,9 @@ platform | result
             if (key === 'IOS') {
                 return 'failed';
             }
+            if (key === 'DESKTOP') {
+                return 'cancelled';
+            }
             return mockGetInputDefaultImplementation(key);
         });
 
@@ -309,6 +316,7 @@ platform | result
 
 platform | result
 ---|---
+🖥 desktop 🖥|cancelled 🔪
 🕸 web 🕸|success ✅
 🤖 android 🤖|skipped 🚫
 🍎 iOS 🍎|failed ❌`,
diff --git a/tests/unit/postTestBuildComment.ts b/tests/unit/postTestBuildComment.ts
index f7caf2dc6f61..137bf8def350 100644
--- a/tests/unit/postTestBuildComment.ts
+++ b/tests/unit/postTestBuildComment.ts
@@ -46,39 +46,41 @@ jest.mock('@actions/github', () => ({
 const androidLink = 'https://expensify.app/ANDROID_LINK';
 const iOSLink = 'https://expensify.app/IOS_LINK';
 const webLink = 'https://expensify.app/WEB_LINK';
+const desktopLink = 'https://expensify.app/DESKTOP_LINK';
 
 const androidQRCode = `![Android](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${androidLink})`;
+const desktopQRCode = `![Desktop](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${desktopLink})`;
 const iOSQRCode = `![iOS](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${iOSLink})`;
 const webQRCode = `![Web](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${webLink})`;
 
-const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! :test_tube::test_tube:
+const message = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing! :test_tube::test_tube:
 Built from App PR Expensify/App#12 Mobile-Expensify PR Expensify/Mobile-Expensify#13.
 | Android :robot:  | iOS :apple: |
 | ------------- | ------------- |
 | ${androidLink}  | ${iOSLink}  |
 | ${androidQRCode}  | ${iOSQRCode}  |
 
-| Web :spider_web: |
-| ------------- |
-| ${webLink}  |
-| ${webQRCode}  |
+| Desktop :computer: | Web :spider_web: |
+| ------------- | ------------- |
+| ${desktopLink}  | ${webLink}  |
+| ${desktopQRCode}  | ${webQRCode}  |
 
 ---
 
 :eyes: [View the workflow run that generated this build](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/1234) :eyes:
 `;
 
-const onlyAppMessage = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! :test_tube::test_tube:
+const onlyAppMessage = `:test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing! :test_tube::test_tube:
 Built from App PR Expensify/App#12.
 | Android :robot:  | iOS :apple: |
 | ------------- | ------------- |
 | ${androidLink}  | ⏩ SKIPPED ⏩  |
 | ${androidQRCode}  | The build for iOS was skipped  |
 
-| Web :spider_web: |
-| ------------- |
-| ⏩ SKIPPED ⏩  |
-| The build for Web was skipped  |
+| Desktop :computer: | Web :spider_web: |
+| ------------- | ------------- |
+| ❌ FAILED ❌  | ⏩ SKIPPED ⏩  |
+| The QR code can't be generated, because the Desktop build failed  | The build for Web was skipped  |
 
 ---
 
@@ -92,10 +94,10 @@ Built from Mobile-Expensify PR Expensify/Mobile-Expensify#13.
 | ${androidLink}  | ${iOSLink}  |
 | ${androidQRCode}  | ${iOSQRCode}  |
 
-| Web :spider_web: |
-| ------------- |
-| ⏩ SKIPPED ⏩  |
-| The build for Web was skipped  |
+| Desktop :computer: | Web :spider_web: |
+| ------------- | ------------- |
+| ⏩ SKIPPED ⏩  | ⏩ SKIPPED ⏩  |
+| The build for Desktop was skipped  | The build for Web was skipped  |
 
 ---
 
@@ -117,14 +119,16 @@ describe('Post test build comments action tests', () => {
         when(core.getInput).calledWith('ANDROID', {required: false}).mockReturnValue('success');
         when(core.getInput).calledWith('IOS', {required: false}).mockReturnValue('success');
         when(core.getInput).calledWith('WEB', {required: false}).mockReturnValue('success');
+        when(core.getInput).calledWith('DESKTOP', {required: false}).mockReturnValue('success');
         when(core.getInput).calledWith('ANDROID_LINK').mockReturnValue(androidLink);
         when(core.getInput).calledWith('IOS_LINK').mockReturnValue(iOSLink);
         when(core.getInput).calledWith('WEB_LINK').mockReturnValue('https://expensify.app/WEB_LINK');
+        when(core.getInput).calledWith('DESKTOP_LINK').mockReturnValue('https://expensify.app/DESKTOP_LINK');
         createCommentMock.mockResolvedValue({} as CreateCommentResponse);
         mockListComments.mockResolvedValue({
             data: [
                 {
-                    body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing!',
+                    body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing!',
                     // eslint-disable-next-line @typescript-eslint/naming-convention
                     node_id: 'IC_abcd',
                 },
@@ -152,12 +156,13 @@ describe('Post test build comments action tests', () => {
         when(core.getInput).calledWith('ANDROID', {required: false}).mockReturnValue('success');
         when(core.getInput).calledWith('IOS', {required: false}).mockReturnValue('skipped');
         when(core.getInput).calledWith('WEB', {required: false}).mockReturnValue('skipped');
+        when(core.getInput).calledWith('DESKTOP', {required: false}).mockReturnValue('failure');
         when(core.getInput).calledWith('ANDROID_LINK').mockReturnValue('https://expensify.app/ANDROID_LINK');
         createCommentMock.mockResolvedValue({} as CreateCommentResponse);
         mockListComments.mockResolvedValue({
             data: [
                 {
-                    body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing!',
+                    body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing!',
                     // eslint-disable-next-line @typescript-eslint/naming-convention
                     node_id: 'IC_abcd',
                 },
@@ -187,6 +192,7 @@ describe('Post test build comments action tests', () => {
         when(core.getInput).calledWith('ANDROID_LINK').mockReturnValue(androidLink);
         when(core.getInput).calledWith('IOS_LINK').mockReturnValue(iOSLink);
         when(core.getInput).calledWith('WEB', {required: false}).mockReturnValue('skipped');
+        when(core.getInput).calledWith('DESKTOP', {required: false}).mockReturnValue('skipped');
         createCommentMock.mockResolvedValue({} as CreateCommentResponse);
         mockListComments.mockResolvedValue({
             data: [
diff --git a/tsconfig.json b/tsconfig.json
index f8058fb17f35..7e1dba141d7c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -34,6 +34,7 @@
     },
     "include": [
         "src",
+        "desktop",
         "web",
         "website",
         "docs",
