Files
trackgo-fe/apps/desktop/docs/CONFIGURATION.md
T
Firman Ramdhani d3eb242ebe docs: Enhance documentation across multiple modules for clarity and structure
- Updated CONFIGURATION.md to improve navigation and added mermaid diagrams for better visualization of processes.
- Revised IPC_ARCHITECTURE.md to clarify the security model and added diagrams to illustrate the architecture.
- Improved README.md files in core-api, core-events, core-i18n, and core-storage for consistency and clarity, including better descriptions and structural enhancements.
2026-05-29 16:21:25 +07:00

16 KiB

← Back to Root

Desktop Configuration Guide

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

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

    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:

    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.

graph TD
    %% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
    classDef webApp fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
    classDef bridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
    classDef electronApp fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
    classDef finalArtifact fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff

    %% ─── Nodes ───
    SOURCE[(apps/TARGET/dist/)]
    SCRIPT{copy-web-dist.ts}
    DEST[(apps/desktop/web-dist/)]
    BUILDER(electron-builder)
    OUTPUT([Packaged .app / .exe])

    %% ─── Flow ───
    SOURCE ===>|Vite Build Output| SCRIPT
    SCRIPT ===>|Deployment Bridge Prebuild Hook| DEST
    DEST -.->|files and extraResources| BUILDER
    BUILDER ===> OUTPUT

    %% ─── Apply Styles ───
    class SOURCE webApp;
    class SCRIPT bridge;
    class DEST,BUILDER electronApp;
    class OUTPUT finalArtifact;

🔐 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

[!IMPORTANT] > 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:

graph TD
    %% ─── Styling Definitions ───
    classDef request fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
    classDef process fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
    classDef decision fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
    classDef success fill:#3b82f6,stroke:#1d4ed8,stroke-width:2px,color:#ffffff

    %% ─── Nodes ───
    REQ([Request: app://-/settings/profile])
    DECODE[Decode URI and Normalize Path]
    CHECK{File exists in web-dist?}
    SERVE_FILE[Serve Asset with MIME + CSP]
    SERVE_FALLBACK[Heuristic Fallback: Serve index.html]
    REACT([React Router Handles Route])

    %% ─── Flow ───
    REQ ---> DECODE
    DECODE ---> CHECK
    CHECK ===>|YES| SERVE_FILE
    CHECK -.->|NO| SERVE_FALLBACK
    SERVE_FALLBACK ---> REACT

    %% ─── Apply Styles ───
    class REQ request;
    class DECODE process;
    class CHECK decision;
    class SERVE_FILE,SERVE_FALLBACK,REACT success;

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

[!CAUTION] > 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, #/showcase.

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