# 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](#target-app-configuration) - [Environment Variables](#environment-variables) - [Production SPA Routing (Custom `app://` Protocol)](#production-spa-routing-custom-app-protocol) - [Break Glass: Reverting to `file://` + HashRouter](#break-glass-reverting-to-file--hashrouter) --- ## 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 ```env # 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`: ```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: ```bash 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//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//dist/ → apps/desktop/web-dist/ Development (main process): __dirname (out/main/) → ../../ → apps/ → apps//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`): ```diff - import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; + import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; export default function App() { return ( - + Loading...}> {/* All route definitions remain unchanged */} - + ); } ``` 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: ```diff - 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()`: ```diff 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 `` tag to the web app's `index.html` since there's no protocol handler to inject headers: ```html ``` ### 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 `` tag | | Third-party compatibility | Rare edge cases | Maximum compatibility |