Desktop Configuration Guide โ
Architectural Foundation: Electron Protocol API ยท electron-builder ยท React Router
Description: Runtime configuration guide for the Desktop Wrapper covering target app orchestration, deterministic path resolution, custom app:// protocol routing with SPA fallback, and the HashRouter disaster recovery procedure.
The Blueprint for Runtime Control.
This guide defines the operational parameters of the Desktop Wrapper. It governs the orchestration of target applications, encapsulates the mechanics of our proprietary production routing, and provides a fail-safe Break Glass Procedure for emergency infrastructure transitions.
Table of Contents โ
- Target App Orchestration
- Environment Variables Registry
- Deterministic Path Resolution
- Production Routing: Overcoming Protocol Constraints
- Defense-in-Depth: Multi-Layered Protection
- Break Glass Procedure: Disaster Recovery Protocol
๐ฏ Target App Orchestration โ
The Desktop Wrapper is architected to embed any web application within the monorepo ecosystem. The target application is resolved at build time through a declarative configuration surface in apps/desktop/.env.
.env Declaration โ
# The workspace identifier of the target web application.
# Must correspond to a directory under apps/ (e.g., "web", "docs-dev", "admin").
DESKTOP_TARGET_APP=web
# The Vite development server endpoint for the target application.
DESKTOP_DEV_SERVER_URL=http://localhost:5173Switching the Target Application โ
To redirect the wrapper to a different application โ for example, apps/admin โ modify the configuration and re-execute the build pipeline:
Update the
.envdeclaration:envDESKTOP_TARGET_APP=admin DESKTOP_DEV_SERVER_URL=http://localhost:3001Verify the target app exports a
buildscript that emits static assets todist/.Execute the deterministic build pipeline:
bashpnpm build --filter=admin && cd apps/desktop && pnpm run build
The Deployment Bridge โ
The prebuild hook invokes scripts/copy-web-dist.ts, which serves as the Deployment Bridge between the web workspace and the native container. It reads DESKTOP_TARGET_APP, resolves the corresponding apps/<target>/dist/ directory, and synchronizes the contents into apps/desktop/web-dist/. This bridge directory is then ingested by electron-builder.
๐ Environment Variables Registry โ
| Variable | Default | Security Scope | Consumer | Description |
|---|---|---|---|---|
DESKTOP_TARGET_APP | web | Build-time | copy-web-dist.ts | Workspace identifier of the web app to embed |
DESKTOP_DEV_SERVER_URL | http://localhost:5173 | Runtime (dev) | src/main/index.ts | Dev server URL loaded in the Electron window during development |
GH_TOKEN | โ | CI/CD | electron-builder | GitHub personal access token for publishing releases |
CSC_LINK | โ | CI/CD | electron-builder | Base64-encoded .p12 code signing certificate |
CSC_KEY_PASSWORD | โ | CI/CD | electron-builder | Passphrase for the .p12 certificate |
APPLE_ID | โ | CI/CD (macOS) | electron-builder | Apple ID email for notarization submission |
APPLE_APP_SPECIFIC_PASSWORD | โ | CI/CD (macOS) | electron-builder | App-specific password for notarization |
APPLE_TEAM_ID | โ | CI/CD (macOS) | electron-builder | Apple Developer Team ID |
> **Build-time** variables are consumed during the `prebuild` phase and baked into the artifact. **Runtime** variables are read by the Electron main process at launch. **CI/CD** variables are secrets injected exclusively in the deployment environment โ they must never appear in source control or local `.env` files.
๐ Deterministic Path Resolution โ
The following matrix defines how the target app's static assets are resolved across every phase of the application lifecycle. Each path is deterministic โ there is no runtime ambiguity.
| Phase | Resolution Strategy | Resolved Path | Context |
|---|---|---|---|
| Prebuild | copy-web-dist.ts reads DESKTOP_TARGET_APP | monorepo-root/apps/<target>/dist/ โ apps/desktop/web-dist/ | Deployment Bridge: build-time synchronization |
| Development | __dirname relative traversal from out/main/ | apps/desktop/out/main/ โ ../../ โ apps/ โ <target>/dist/ | Direct filesystem access to the web app's build output |
| Production | process.resourcesPath | Contents/Resources/web-dist/ (macOS) / resources/web-dist/ (Windows/Linux) | OS-specific resource directory within the packaged binary |
NOTE
The development path relies on __dirname pointing to apps/desktop/out/main/ at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in getWebDistPath() within src/main/index.ts.
๐ Production Routing: Overcoming Protocol Constraints โ
The Constraint โ
React applications using BrowserRouter rely on a fundamental server-side contract: every URL path must return index.html. Paths like /dashboard, /auth/login, and /settings/profile do not correspond to physical files โ they are virtual routes resolved entirely by the client-side router.
Electron's default file:// protocol breaks this contract. Requesting file:///app/dashboard triggers a literal filesystem lookup for a file named dashboard, which does not exist, resulting in a blank screen or an OS-level "file not found" error.
The Solution: A Privileged Virtual File System โ
The app:// scheme is a Privileged Virtual File System that resolves SPA routing conflicts by implementing a Heuristic Resource Loader. It operates as follows:
If a requested URI does not map to a physical asset, the handler intelligently intercepts the request to serve the index.html entry point, allowing React Router to maintain stateful client-side navigation. This ensures that deep links, page refreshes, and direct URL entry all function without modification to the React app's routing configuration.
Scheme Registration โ
The scheme must be registered synchronously at module load time, before app.whenReady(). This is a Chromium requirement โ deferred registration will silently fail:
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true, // Enables URL parsing (host, path, query)
secure: true, // Treated as a secure origin (HTTPS equivalent)
supportFetchAPI: true, // Allows fetch() from this scheme
corsEnabled: true, // Enables CORS for cross-origin requests
stream: true, // Supports streaming responses
},
},
]);๐ก๏ธ Defense-in-Depth: Multi-Layered Protection โ
The custom protocol handler enforces a multi-layered defense perimeter that goes beyond standard Electron security defaults.
| Layer | Technique | Implementation | Threat Mitigated |
|---|---|---|---|
| I/O Sanitization | Path traversal guard | normalize() + startsWith() validation against web-dist/ boundary | Directory traversal attacks (../../etc/passwd) โ 403 Forbidden |
| In-Flight Policy Injection | CSP response headers | Content-Security-Policy injected as HTTP response headers on every HTML payload | XSS execution via script injection |
| Cryptographic Isolation | Privileged scheme registration | app scheme registered with standard, secure, supportFetchAPI, corsEnabled | Scheme downgrade attacks; the renderer treats app:// identically to https:// |
| Resource Type Validation | statSync.isFile() check | Only regular files are served; directories return the SPA fallback | Information disclosure via directory listing |
| Origin Sanitization | CORS bypass proxy | webRequest.onBeforeSendHeaders strips app:// Origin headers on outgoing requests | Backend CORS rejection of non-standard origins |
| Navigation Confinement | will-navigate guard | Blocks navigation to URLs outside app:// and the authorized dev server | Phishing via in-app redirect to malicious sites |
โ ๏ธ Break Glass Procedure: Disaster Recovery Protocol โ
> **This is a formal Disaster Recovery Protocol.** Execute only if the custom `app://` protocol causes an irrecoverable failure โ for example, a critical third-party library that refuses to operate under a non-standard URI scheme. This procedure requires coordinated changes across both the React application and the Electron main process. Estimated recovery time: **15 minutes**.
Step 1: Switch the Router โ React Application โ
In the target web app's entry point (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>
);
}All routes transition to hash-based addressing: #/app/dashboard, #/auth/login.
Step 2: Decommission the Custom Protocol โ Main Process โ
In apps/desktop/src/main/index.ts, execute the following surgical removals:
a) Remove the scheme registration block at the top of the file:
- protocol.registerSchemesAsPrivileged([ ... ]);b) Delete the entire registerAppProtocol() function.
c) Remove the registerAppProtocol() invocation inside app.whenReady().
d) Redirect 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) Inject a CSP <meta> tag into the web app's index.html, since the In-Flight Policy Injection layer is no longer available:
<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:;"
/>Trade-off Analysis โ
| Dimension | Custom app:// Protocol | file:// + HashRouter |
|---|---|---|
| Aesthetic Integrity | Clean URLs: /app/dashboard | Hash prefix: #/app/dashboard |
| Router Compatibility | BrowserRouter โ zero changes required | Must migrate to HashRouter |
| Deep Linking | Full, native-style support | Hash-based only |
| Protocol-Native Compatibility | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility |
| Security Delivery Vector | CSP via response headers (strongest enforcement) | CSP via <meta> tag (bypassable by early script execution) |
| Implementation Complexity | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
| Recovery Time | โ | ~15 minutes, 2 files |