Files
trackgo-fe/apps/desktop/docs/CONFIGURATION.md
T

6.5 KiB

Configuration Guide

This document covers how to configure the Electron desktop wrapper, manage the target web application, and handle production SPA routing.


Table of Contents


Target App Configuration

The desktop app wraps any web application in the monorepo. The target is configured via environment variables in apps/desktop/.env.

.env File

# The workspace name of the target web app to wrap.
# Must match a directory under apps/ (e.g., "web", "docs-dev", "admin").
DESKTOP_TARGET_APP=web

# The dev server URL for the target web app.
# This is the URL that Vite serves during development.
DESKTOP_DEV_SERVER_URL=http://localhost:5173

Switching to a Different App

To wrap apps/admin instead of apps/web:

  1. Update .env:

    DESKTOP_TARGET_APP=admin
    DESKTOP_DEV_SERVER_URL=http://localhost:3001
    
  2. Ensure the target app has a build script that outputs to dist/.

  3. Run the desktop build:

    pnpm build --filter=admin && cd apps/desktop && pnpm run build
    

The scripts/copy-web-dist.ts prebuild script reads DESKTOP_TARGET_APP and copies apps/<target>/dist/ into apps/desktop/web-dist/, which is then bundled by electron-builder.

How Path Resolution Works

Prebuild (copy-web-dist.ts):
  monorepo-root/apps/<DESKTOP_TARGET_APP>/dist/  →  apps/desktop/web-dist/

Development (main process):
  __dirname (out/main/) → ../../ → apps/ → apps/<target>/dist/

Production (packaged app):
  process.resourcesPath → Contents/Resources/web-dist/

Environment Variables

Variable Default Used By Description
DESKTOP_TARGET_APP web copy-web-dist.ts Workspace name of the web app to embed
DESKTOP_DEV_SERVER_URL http://localhost:5173 src/main/index.ts URL of the target app's Vite dev server
GH_TOKEN electron-builder GitHub token for publishing releases
CSC_LINK electron-builder Base64-encoded code signing certificate
CSC_KEY_PASSWORD electron-builder Password for the signing certificate

Production SPA Routing (Custom app:// Protocol)

The Problem

React apps using BrowserRouter rely on the server to always return index.html for any URL path (e.g., /dashboard, /auth/login). With Electron's file:// protocol, requesting file:///app/dashboard looks for an actual file at that path — which doesn't exist — resulting in a blank screen or "file not found" error.

The Solution

The main process registers a custom app:// protocol with a handler that:

  1. Receives a request like app://-/dashboard.
  2. Strips the protocol and hostname to get the path: dashboard.
  3. Checks if a real file exists at web-dist/dashboard.
  4. If yes → serves the file with the correct MIME type.
  5. If no → serves web-dist/index.html instead (SPA fallback).

This allows React Router to handle all client-side routing normally. Deep links, page refreshes, and direct URL entry all work because every unknown path falls back to index.html.

Security Measures

  • Path traversal protection: The resolved file path is validated to stay within web-dist/ using normalize() + startsWith() check. Requests like app://-/../../etc/passwd return 403 Forbidden.
  • CSP headers: Content-Security-Policy headers are injected on every HTML response served by the protocol handler.
  • Scheme privileges: The app scheme is registered with standard: true, secure: true, supportFetchAPI: true, and corsEnabled: true — making it behave like https:// to the renderer process.

Break Glass: Reverting to file:// + HashRouter

If the custom app:// protocol ever causes issues (e.g., a third-party library incompatibility), you can fall back to the standard file:// protocol with HashRouter. This requires two changes.

Step 1: Switch Router in the React App

In the target web app (e.g., apps/web/src/apps/index.tsx):

- import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
+ import { HashRouter, Navigate, Route, Routes } from 'react-router-dom';

  export default function App() {
    return (
      <ThemeProvider colorScheme={colorScheme} density={density}>
-       <BrowserRouter>
+       <HashRouter>
          <Suspense fallback={<div>Loading...</div>}>
            <Routes>
              {/* All route definitions remain unchanged */}
            </Routes>
          </Suspense>
-       </BrowserRouter>
+       </HashRouter>
      </ThemeProvider>
    );
  }

Routes will now use hash-based URLs: #/app/dashboard, #/auth/login, #/showcase.

Step 2: Switch to file:// in the Main Process

In apps/desktop/src/main/index.ts:

a) Remove the scheme registration at the top of the file:

- protocol.registerSchemesAsPrivileged([
-   {
-     scheme: 'app',
-     privileges: { standard: true, secure: true, ... },
-   },
- ]);

b) Remove the registerAppProtocol() function entirely.

c) Remove the registerAppProtocol() call in app.whenReady().

d) Change the production content loading in createWindow():

  if (IS_DEV) {
    mainWindow.loadURL(DEV_SERVER_URL);
    mainWindow.webContents.openDevTools({ mode: 'detach' });
  } else {
-   mainWindow.loadURL('app://-/index.html');
+   const webDistPath = getWebDistPath();
+   mainWindow.loadFile(join(webDistPath, 'index.html'));
  }

e) Add a CSP <meta> tag to the web app's index.html since there's no protocol handler to inject headers:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self' file:; script-src 'self' file:; style-src 'self' 'unsafe-inline' file:; connect-src 'self' https:; img-src 'self' file: data: https:; font-src 'self' file: data:;" />

Comparison

Aspect Custom app:// file:// + HashRouter
URL appearance /app/dashboard #/app/dashboard
React Router BrowserRouter (no change) Must use HashRouter
Deep linking Full support Hash-based
Implementation complexity Higher Lower
CSP delivery Via response headers Via <meta> tag
Third-party compatibility Rare edge cases Maximum compatibility