Skip to content

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 โ€‹

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 โ€‹

env
# 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:5173

Switching the Target Application โ€‹

To redirect the wrapper to a different application โ€” for example, apps/admin โ€” modify the configuration and re-execute the build pipeline:

  1. Update the .env declaration:

    env
    DESKTOP_TARGET_APP=admin
    DESKTOP_DEV_SERVER_URL=http://localhost:3001
  2. Verify the target app exports a build script that emits static assets to dist/.

  3. Execute the deterministic build pipeline:

    bash
    pnpm 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 โ€‹

VariableDefaultSecurity ScopeConsumerDescription
DESKTOP_TARGET_APPwebBuild-timecopy-web-dist.tsWorkspace identifier of the web app to embed
DESKTOP_DEV_SERVER_URLhttp://localhost:5173Runtime (dev)src/main/index.tsDev server URL loaded in the Electron window during development
GH_TOKENโ€”CI/CDelectron-builderGitHub personal access token for publishing releases
CSC_LINKโ€”CI/CDelectron-builderBase64-encoded .p12 code signing certificate
CSC_KEY_PASSWORDโ€”CI/CDelectron-builderPassphrase for the .p12 certificate
APPLE_IDโ€”CI/CD (macOS)electron-builderApple ID email for notarization submission
APPLE_APP_SPECIFIC_PASSWORDโ€”CI/CD (macOS)electron-builderApp-specific password for notarization
APPLE_TEAM_IDโ€”CI/CD (macOS)electron-builderApple 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.

PhaseResolution StrategyResolved PathContext
Prebuildcopy-web-dist.ts reads DESKTOP_TARGET_APPmonorepo-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
Productionprocess.resourcesPathContents/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:

typescript
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.

LayerTechniqueImplementationThreat Mitigated
I/O SanitizationPath traversal guardnormalize() + startsWith() validation against web-dist/ boundaryDirectory traversal attacks (../../etc/passwd) โ†’ 403 Forbidden
In-Flight Policy InjectionCSP response headersContent-Security-Policy injected as HTTP response headers on every HTML payloadXSS execution via script injection
Cryptographic IsolationPrivileged scheme registrationapp scheme registered with standard, secure, supportFetchAPI, corsEnabledScheme downgrade attacks; the renderer treats app:// identically to https://
Resource Type ValidationstatSync.isFile() checkOnly regular files are served; directories return the SPA fallbackInformation disclosure via directory listing
Origin SanitizationCORS bypass proxywebRequest.onBeforeSendHeaders strips app:// Origin headers on outgoing requestsBackend CORS rejection of non-standard origins
Navigation Confinementwill-navigate guardBlocks navigation to URLs outside app:// and the authorized dev serverPhishing 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):

diff
- 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:

diff
- protocol.registerSchemesAsPrivileged([ ... ]);

b) Delete the entire registerAppProtocol() function.

c) Remove the registerAppProtocol() invocation inside app.whenReady().

d) Redirect 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) Inject a CSP <meta> tag into the web app's index.html, since the In-Flight Policy Injection layer is no longer available:

html
<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 โ€‹

DimensionCustom app:// Protocolfile:// + HashRouter
Aesthetic IntegrityClean URLs: /app/dashboardHash prefix: #/app/dashboard
Router CompatibilityBrowserRouter โ€” zero changes requiredMust migrate to HashRouter
Deep LinkingFull, native-style supportHash-based only
Protocol-Native CompatibilityRare edge cases with non-standard scheme detectionMaximum third-party compatibility
Security Delivery VectorCSP via response headers (strongest enforcement)CSP via <meta> tag (bypassable by early script execution)
Implementation ComplexityHigher (custom protocol handler + security layers)Lower (no custom protocol infrastructure)
Recovery Timeโ€”~15 minutes, 2 files