From 8cac18edf49974d34138832e79f03921858a3468 Mon Sep 17 00:00:00 2001
From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com>
Date: Sun, 5 Apr 2026 23:03:40 +0700
Subject: [PATCH] docs: add documentation for desktop auto-updater,
configuration, IPC architecture, and project overview
---
apps/desktop/README.md | 127 +++++++++++
apps/desktop/docs/AUTO_UPDATER.md | 310 ++++++++++++++++++++++++++
apps/desktop/docs/CONFIGURATION.md | 183 +++++++++++++++
apps/desktop/docs/IPC_ARCHITECTURE.md | 290 ++++++++++++++++++++++++
4 files changed, 910 insertions(+)
create mode 100644 apps/desktop/README.md
create mode 100644 apps/desktop/docs/AUTO_UPDATER.md
create mode 100644 apps/desktop/docs/CONFIGURATION.md
create mode 100644 apps/desktop/docs/IPC_ARCHITECTURE.md
diff --git a/apps/desktop/README.md b/apps/desktop/README.md
new file mode 100644
index 0000000..b1f87ec
--- /dev/null
+++ b/apps/desktop/README.md
@@ -0,0 +1,127 @@
+# Eigen Desktop
+
+An Electron wrapper for the web applications in this monorepo. Powered by **electron-vite** for development and **electron-builder** for production packaging.
+
+---
+
+## Quick Start
+
+```bash
+# From the monorepo root
+
+# Install dependencies
+pnpm install
+
+# Development (starts both the web dev server and Electron)
+pnpm dev:desktop
+
+# Build for production
+pnpm build:desktop
+
+# Package for distribution
+pnpm package:desktop
+```
+
+---
+
+## How It Works
+
+| Environment | Behavior |
+|---|---|
+| **Development** | Electron loads the Vite dev server (`http://localhost:5173`). Hot reload works normally. |
+| **Production** | Electron registers a custom `app://` protocol that serves the static build output of the target web app. SPA client-side routing is fully supported via an `index.html` fallback. |
+
+---
+
+## Project Structure
+
+```
+apps/desktop/
+├── docs/ # Documentation
+│ ├── CONFIGURATION.md # Target app, routing, HashRouter fallback
+│ ├── AUTO_UPDATER.md # Release process, CI/CD, code signing
+│ └── IPC_ARCHITECTURE.md # Security model, adding new features
+├── scripts/
+│ └── copy-web-dist.ts # Prebuild: copies web app build → web-dist/
+├── src/
+│ ├── main/
+│ │ └── index.ts # Main process: protocol, CORS, IPC, updater
+│ ├── preload/
+│ │ └── index.ts # Secure contextBridge API
+│ └── renderer/
+│ └── index.html # Renderer shell (CSP reference)
+├── .env # Target app configuration
+├── electron-builder.yml # Packaging & auto-update config
+├── electron-vite.config.ts # Build config (main, preload, renderer)
+├── package.json
+├── tsconfig.json
+├── tsconfig.main.json
+├── tsconfig.preload.json
+└── tsconfig.renderer.json
+```
+
+---
+
+## Scripts
+
+| Script | Description |
+|---|---|
+| `pnpm dev` | Start electron-vite dev server |
+| `pnpm build` | Compile TypeScript → `out/` |
+| `pnpm prebuild` | Copy target web app's `dist/` → `web-dist/` |
+| `pnpm package` | Build + package for current platform |
+| `pnpm package:win` | Package for Windows (NSIS) |
+| `pnpm package:mac` | Package for macOS (DMG + ZIP) |
+| `pnpm package:linux` | Package for Linux (AppImage) |
+
+---
+
+## Configuration
+
+The target web app is configured via `.env`:
+
+```env
+DESKTOP_TARGET_APP=web
+DESKTOP_DEV_SERVER_URL=http://localhost:5173
+```
+
+See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for details on switching target apps and routing fallbacks.
+
+---
+
+## Features
+
+### Custom `app://` Protocol
+Serves the web app's static build with SPA routing support. Includes path traversal protection and CSP header injection.
+
+### Hardware Printing
+The React app can list printers and trigger print jobs via `window.electronAPI.getPrinters()` and `window.electronAPI.print()`.
+
+### Auto-Update
+Background update checks via GitHub Releases with download progress forwarding to the React UI. See [docs/AUTO_UPDATER.md](docs/AUTO_UPDATER.md).
+
+### CORS Bypass
+API requests from the `app://` origin are transparently handled by stripping non-standard Origin headers and injecting CORS response headers.
+
+---
+
+## Security
+
+- `contextIsolation: true` — preload runs in an isolated context
+- `nodeIntegration: false` — no Node.js APIs in the renderer
+- `sandbox: true` — Chromium sandbox enabled
+- `webSecurity: true` — same-origin policy enforced
+- Path traversal protection in the custom protocol handler
+- CSP headers injected on all HTML responses
+
+See [docs/IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) for the full security model and how to safely extend the app.
+
+---
+
+## Documentation
+
+| Document | Contents |
+|---|---|
+| [CONFIGURATION.md](docs/CONFIGURATION.md) | Target app switching, `app://` protocol internals, HashRouter fallback procedure |
+| [AUTO_UPDATER.md](docs/AUTO_UPDATER.md) | Release workflow, CI/CD variables, S3/generic provider switching, code signing |
+| [IPC_ARCHITECTURE.md](docs/IPC_ARCHITECTURE.md) | Security model, Three-Step Bridge pattern, existing IPC channels, extension guide |
diff --git a/apps/desktop/docs/AUTO_UPDATER.md b/apps/desktop/docs/AUTO_UPDATER.md
new file mode 100644
index 0000000..6a871f8
--- /dev/null
+++ b/apps/desktop/docs/AUTO_UPDATER.md
@@ -0,0 +1,310 @@
+# Auto-Update System
+
+This document covers the Electron auto-update system powered by `electron-updater`, including the release workflow, provider configuration, CI/CD requirements, and code signing.
+
+---
+
+## Table of Contents
+
+- [Architecture Overview](#architecture-overview)
+- [Current Provider: GitHub Releases](#current-provider-github-releases)
+- [Release Workflow](#release-workflow)
+- [CI/CD Environment Variables](#cicd-environment-variables)
+- [Switching to AWS S3](#switching-to-aws-s3)
+- [Switching to a Generic File Server](#switching-to-a-generic-file-server)
+- [Code Signing Requirements](#code-signing-requirements)
+- [Troubleshooting](#troubleshooting)
+
+---
+
+## Architecture Overview
+
+The auto-update flow involves three layers:
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Main Process (src/main/index.ts) │
+│ │
+│ autoUpdater.checkForUpdatesAndNotify() │
+│ ↓ │
+│ Events: checking → available → progress → downloaded │
+│ ↓ │
+│ sendToRenderer('updater:*', data) │
+├─────────────────────────────────────────────────────────┤
+│ Preload (src/preload/index.ts) │
+│ │
+│ contextBridge: onUpdateAvailable, onDownloadProgress, │
+│ onUpdateDownloaded, checkForUpdates, │
+│ installUpdate │
+├─────────────────────────────────────────────────────────┤
+│ Renderer / React (apps/web) │
+│ │
+│ useElectronUpdater() hook │
+│ → status, progress, updateInfo, errorMessage │
+│ → checkForUpdates(), installUpdate() │
+└─────────────────────────────────────────────────────────┘
+```
+
+### Lifecycle
+
+1. **App starts** → After a 3-second delay, `autoUpdater.checkForUpdatesAndNotify()` is called.
+2. **Update available** → If `autoDownload` is `true` (default), downloads automatically.
+3. **Download progress** → `download-progress` events are forwarded to the renderer.
+4. **Update downloaded** → The renderer shows a "Restart to Update" prompt.
+5. **User clicks install** → `autoUpdater.quitAndInstall()` restarts the app with the new version.
+
+---
+
+## Current Provider: GitHub Releases
+
+The update provider is configured in `electron-builder.yml`:
+
+```yaml
+publish:
+ provider: github
+ owner: YOUR_GITHUB_ORG
+ repo: YOUR_REPO_NAME
+```
+
+### How it Works
+
+1. When you run `electron-builder --publish always`, it:
+ - Builds the app for your target platform.
+ - Uploads the installer(s) to a **GitHub Release** tagged with the version from `package.json`.
+ - Generates and uploads `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
+
+2. When the packaged app calls `checkForUpdates()`, `electron-updater`:
+ - Reads `app-update.yml` from the app's `resources/` directory (auto-generated during build).
+ - Fetches the appropriate `latest*.yml` from the configured GitHub release.
+ - Compares versions and downloads the update if a newer version exists.
+
+### `app-update.yml`
+
+This file is **automatically generated** by `electron-builder` during the build process. It contains the provider configuration and is placed in the packaged app's `resources/` directory. You do NOT need to create or manage this file manually.
+
+---
+
+## Release Workflow
+
+### Manual Release
+
+```bash
+# 1. Bump the version
+cd apps/desktop
+npm version patch # or minor, major
+
+# 2. Build the web app
+cd ../..
+pnpm build --filter=web
+
+# 3. Build & publish the Electron app
+cd apps/desktop
+pnpm run prebuild
+GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
+```
+
+### Automated Release (GitHub Actions)
+
+A typical CI workflow:
+
+```yaml
+name: Release Desktop
+
+on:
+ push:
+ tags:
+ - 'desktop-v*'
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+
+ - run: pnpm install
+ - run: pnpm build --filter=web
+ - run: cd apps/desktop && pnpm run prebuild
+ - run: cd apps/desktop && pnpm run build
+
+ - name: Publish
+ run: cd apps/desktop && npx electron-builder --publish always --config electron-builder.yml
+ env:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+```
+
+---
+
+## CI/CD Environment Variables
+
+| Variable | Required | Platform | Description |
+|---|---|---|---|
+| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Used by electron-builder to create/upload releases. |
+| `CSC_LINK` | macOS/Windows | macOS, Windows | Base64-encoded `.p12` code signing certificate. Generate with: `base64 -i cert.p12 \| pbcopy` |
+| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Password for the `.p12` certificate. |
+| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization. |
+| `APPLE_APP_SPECIFIC_PASSWORD` | macOS only | macOS | App-specific password generated at [appleid.apple.com](https://appleid.apple.com). |
+| `APPLE_TEAM_ID` | macOS only | macOS | Your Apple Developer Team ID. |
+| `WIN_CSC_LINK` | Windows only | Windows | Separate Windows code signing certificate (if different from `CSC_LINK`). |
+| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Password for the Windows certificate. |
+
+### Setting Secrets in GitHub Actions
+
+1. Go to **Settings → Secrets and variables → Actions** in your repository.
+2. Add each variable as a **Repository secret**.
+3. Reference them in workflow files as `${{ secrets.VARIABLE_NAME }}`.
+
+### Setting Variables in Turborepo
+
+In `turbo.json`, the build task already has `env` awareness via `"inputs": ["$TURBO_DEFAULT$", ".env*"]`. For CI-specific variables, pass them through the environment — Turborepo does NOT manage CI secrets.
+
+---
+
+## Switching to AWS S3
+
+To use a private S3 bucket instead of GitHub Releases, update `electron-builder.yml`:
+
+```yaml
+publish:
+ provider: s3
+ bucket: your-bucket-name
+ region: ap-southeast-1
+ path: /desktop-releases
+ acl: private
+```
+
+### Additional AWS Environment Variables
+
+| Variable | Description |
+|---|---|
+| `AWS_ACCESS_KEY_ID` | IAM access key with S3 write permissions |
+| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
+
+### S3 Bucket Policy
+
+The bucket must allow public read access to the update files, or you must configure a CloudFront distribution in front of it. `electron-updater` needs to `GET` the `latest*.yml` files without authentication.
+
+Recommended bucket structure:
+```
+your-bucket/desktop-releases/
+ ├── latest.yml (Windows)
+ ├── latest-mac.yml (macOS)
+ ├── latest-linux.yml (Linux)
+ ├── EigenDesktop-Setup-0.2.0.exe
+ ├── EigenDesktop-0.2.0.dmg
+ ├── EigenDesktop-0.2.0-mac.zip
+ └── EigenDesktop-0.2.0.AppImage
+```
+
+---
+
+## Switching to a Generic File Server
+
+For a self-hosted server (Nginx, Caddy, etc.):
+
+```yaml
+publish:
+ provider: generic
+ url: https://updates.your-domain.com/desktop
+```
+
+Your server must host the same file structure as S3 above. On each release, upload the installer files and `latest*.yml` to the server.
+
+### Nginx Example
+
+```nginx
+server {
+ listen 443 ssl;
+ server_name updates.your-domain.com;
+
+ location /desktop/ {
+ alias /var/www/desktop-releases/;
+ autoindex off;
+ add_header Cache-Control "no-cache";
+ }
+}
+```
+
+---
+
+## Code Signing Requirements
+
+> [!WARNING]
+> **macOS auto-updates will FAIL without code signing.** Apple's Gatekeeper will block unsigned apps, and `electron-updater` will refuse to apply updates to unsigned builds. This is enforced by the OS, not by Electron.
+
+### macOS
+
+- Requires an **Apple Developer ID Application** certificate.
+- The `electron-builder.yml` is already configured with:
+ ```yaml
+ mac:
+ hardenedRuntime: true
+ gatekeeperAssess: false
+ entitlements: build/entitlements.mac.plist
+ entitlementsInherit: build/entitlements.mac.plist
+ ```
+- You must create `apps/desktop/build/entitlements.mac.plist`:
+ ```xml
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+
+
+ ```
+- **Notarization** is required for macOS 10.15+. Provide `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
+
+### Windows
+
+- Requires an **EV Code Signing Certificate** or a standard code signing certificate.
+- Without signing, Windows SmartScreen will show a warning to users.
+- EV certificates eliminate SmartScreen warnings immediately; standard certificates build reputation over time.
+
+### Linux
+
+- Code signing is **not required** for Linux.
+- AppImage files work without signatures. However, you can optionally sign with GPG for package managers that support it.
+
+---
+
+## Troubleshooting
+
+### "Update check failed" on startup
+
+- **Cause**: The app is offline, or the update server is unreachable.
+- **Impact**: None — the error is caught and logged. The app continues to function normally.
+- **Verification**: Check the main process console for `[AutoUpdater] Startup check failed (possibly offline)`.
+
+### `app-update.yml` not found in production build
+
+- **Cause**: The `publish` block in `electron-builder.yml` is missing or misconfigured.
+- **Fix**: Ensure the `publish` block exists. Run `electron-builder --publish never` first to verify the file is generated in `release/*/resources/app-update.yml`.
+
+### "Cannot update: code signature is invalid" (macOS)
+
+- **Cause**: The app was not signed or the signature is broken.
+- **Fix**: Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are set correctly in CI. Verify with: `codesign --verify --deep --strict release/mac*/EigenDesktop.app`.
+
+### Updates work on Windows/Linux but not macOS
+
+- **Cause**: macOS requires **both** a signed app AND notarization.
+- **Fix**: Provide all Apple credential environment variables and ensure the `mac.hardenedRuntime` and entitlements are configured.
diff --git a/apps/desktop/docs/CONFIGURATION.md b/apps/desktop/docs/CONFIGURATION.md
new file mode 100644
index 0000000..a03ee57
--- /dev/null
+++ b/apps/desktop/docs/CONFIGURATION.md
@@ -0,0 +1,183 @@
+# 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 |
diff --git a/apps/desktop/docs/IPC_ARCHITECTURE.md b/apps/desktop/docs/IPC_ARCHITECTURE.md
new file mode 100644
index 0000000..fad502c
--- /dev/null
+++ b/apps/desktop/docs/IPC_ARCHITECTURE.md
@@ -0,0 +1,290 @@
+# IPC Architecture & Security Model
+
+This document explains the security model of the Electron desktop wrapper, documents the existing IPC channels, and provides a step-by-step guide for extending the app with new native features.
+
+---
+
+## Table of Contents
+
+- [Security Model](#security-model)
+- [The Three-Step Bridge Pattern](#the-three-step-bridge-pattern)
+- [Existing IPC Channels](#existing-ipc-channels)
+- [Adding a New Feature: Step-by-Step Example](#adding-a-new-feature-step-by-step-example)
+- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
+
+---
+
+## Security Model
+
+The Electron desktop wrapper enforces a strict security boundary between the main process (Node.js) and the renderer process (web app). This is critical because the renderer runs untrusted web content that could be compromised by XSS, malicious dependencies, or supply chain attacks.
+
+### Core Principles
+
+| Setting | Value | Why |
+|---|---|---|
+| `contextIsolation` | `true` | The preload script runs in an **isolated JavaScript context**. The renderer cannot access Node.js APIs, `require()`, or the preload's scope. |
+| `nodeIntegration` | `false` | Node.js APIs (`fs`, `child_process`, `os`, etc.) are **completely unavailable** in the renderer. |
+| `sandbox` | `true` | The renderer process runs in a Chromium sandbox with restricted OS-level access. |
+| `webSecurity` | `true` | Same-origin policy is enforced. Cross-origin requests follow standard browser rules. |
+
+### What This Means in Practice
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ Main Process │
+│ Full Node.js access: filesystem, printers, native APIs, │
+│ auto-updater, child processes, network (unrestricted) │
+│ │
+│ ipcMain.handle('channel', handler) │
+├──────────────────────────────────────────────────────────────────┤
+│ Preload Script │
+│ Isolated context. Can use ipcRenderer (send/invoke only). │
+│ Exposes a MINIMAL API surface via contextBridge. │
+│ │
+│ contextBridge.exposeInMainWorld('electronAPI', { ... }) │
+├──────────────────────────────────────────────────────────────────┤
+│ Renderer (React App) │
+│ Standard browser environment. NO Node.js access. │
+│ Can ONLY call methods on window.electronAPI. │
+│ Cannot access ipcRenderer, require, fs, etc. │
+│ │
+│ window.electronAPI.someMethod() │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+The renderer communicates with the main process **only** through the API surface defined in the preload script. This API surface is deliberately narrow — each exposed method does exactly one thing.
+
+---
+
+## The Three-Step Bridge Pattern
+
+Every native feature follows the same three-step pattern:
+
+### Step 1: Register the Handler in the Main Process
+
+File: `apps/desktop/src/main/index.ts`
+
+```typescript
+// Use ipcMain.handle for request/response (returns a value)
+ipcMain.handle('feature:action', async (_event, arg1, arg2) => {
+ // Perform the native operation
+ const result = await someNativeAPI(arg1, arg2);
+ return result;
+});
+
+// Use ipcMain.on for fire-and-forget (no return value)
+ipcMain.on('feature:fire', (_event, data) => {
+ doSomething(data);
+});
+```
+
+**Naming convention**: Use `namespace:action` format. Examples: `printer:get-list`, `updater:check`, `fs:read-file`.
+
+### Step 2: Expose via contextBridge in the Preload Script
+
+File: `apps/desktop/src/preload/index.ts`
+
+```typescript
+const electronAPI = {
+ // For request/response channels
+ featureAction: (arg1: string, arg2: number): Promise => {
+ return ipcRenderer.invoke('feature:action', arg1, arg2);
+ },
+
+ // For fire-and-forget channels
+ featureFire: (data: SomeType): void => {
+ ipcRenderer.send('feature:fire', data);
+ },
+
+ // For main→renderer events (push notifications)
+ onFeatureEvent: createEventSubscription('feature:event'),
+};
+
+contextBridge.exposeInMainWorld('electronAPI', electronAPI);
+```
+
+**Rules**:
+- Never expose `ipcRenderer` directly.
+- Never expose `ipcRenderer.on` — use the `createEventSubscription()` helper that returns an unsubscribe function.
+- Always specify TypeScript types for function signatures.
+
+### Step 3: Update TypeScript Declarations in the React App
+
+File: `apps/web/src/types/electron.d.ts`
+
+```typescript
+interface ElectronAPI {
+ // ... existing methods ...
+
+ // New feature
+ featureAction: (arg1: string, arg2: number) => Promise;
+ featureFire: (data: SomeType) => void;
+ onFeatureEvent: (callback: (data: EventDataType) => void) => () => void;
+}
+```
+
+All three files must stay in sync. If you add a channel to the main process, you must expose it in the preload and declare it in the type file.
+
+---
+
+## Existing IPC Channels
+
+### Printer Channels
+
+| Channel | Direction | Type | Description |
+|---|---|---|---|
+| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` — list of all connected printers. |
+| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Triggers a print job with given options. Returns `{ success, failureReason? }`. |
+
+**Main process implementation**: `setupPrinterIPC()` in `src/main/index.ts`
+
+**Preload exposure**:
+```typescript
+getPrinters: () => ipcRenderer.invoke('printer:get-list')
+print: (options?) => ipcRenderer.invoke('printer:print', options)
+```
+
+**React hook**: `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
+
+---
+
+### Auto-Updater Channels
+
+| Channel | Direction | Type | Description |
+|---|---|---|---|
+| `updater:check` | Renderer → Main | `invoke` / `handle` | Triggers a manual update check. Returns the check result. |
+| `updater:install` | Renderer → Main | `send` / `on` | Quits the app and installs the downloaded update. |
+| `updater:checking` | Main → Renderer | `send` | Emitted when the updater starts checking. |
+| `updater:available` | Main → Renderer | `send` | Emitted when an update is found. Payload: `UpdateInfo`. |
+| `updater:not-available` | Main → Renderer | `send` | Emitted when the app is up to date. Payload: `UpdateInfo`. |
+| `updater:progress` | Main → Renderer | `send` | Emitted during download. Payload: `ProgressInfo`. |
+| `updater:downloaded` | Main → Renderer | `send` | Emitted when download completes. Payload: `UpdateInfo`. |
+| `updater:error` | Main → Renderer | `send` | Emitted on error. Payload: error message string. |
+
+**Main process implementation**: `setupAutoUpdaterIPC()` and `setupAutoUpdaterEvents()` in `src/main/index.ts`
+
+**Preload exposure**: `checkForUpdates()`, `installUpdate()`, `onUpdateAvailable()`, `onDownloadProgress()`, `onUpdateDownloaded()`, `onUpdateError()`, `onUpdateChecking()`, `onUpdateNotAvailable()`
+
+**React hook**: `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
+
+---
+
+## Adding a New Feature: Step-by-Step Example
+
+**Scenario**: Add a method to read the app's version from the main process.
+
+### 1. Main Process
+
+In `src/main/index.ts`, add inside `app.whenReady()`:
+
+```typescript
+ipcMain.handle('app:get-version', () => {
+ return app.getVersion();
+});
+```
+
+### 2. Preload Script
+
+In `src/preload/index.ts`, add to the `electronAPI` object:
+
+```typescript
+const electronAPI = {
+ // ... existing methods ...
+
+ getAppVersion: (): Promise => {
+ return ipcRenderer.invoke('app:get-version');
+ },
+};
+```
+
+### 3. TypeScript Declarations
+
+In `apps/web/src/types/electron.d.ts`, add to the `ElectronAPI` interface:
+
+```typescript
+interface ElectronAPI {
+ // ... existing methods ...
+
+ getAppVersion: () => Promise;
+}
+```
+
+### 4. React Usage
+
+```tsx
+function VersionDisplay() {
+ const [version, setVersion] = useState('');
+
+ useEffect(() => {
+ if (window.electronAPI) {
+ window.electronAPI.getAppVersion().then(setVersion);
+ }
+ }, []);
+
+ if (!version) return null;
+ return v{version};
+}
+```
+
+---
+
+## Anti-Patterns to Avoid
+
+### ❌ Never expose `ipcRenderer` directly
+
+```typescript
+// BAD — gives the renderer unrestricted IPC access
+contextBridge.exposeInMainWorld('ipc', ipcRenderer);
+```
+
+### ❌ Never expose `require` or Node.js APIs
+
+```typescript
+// BAD — allows arbitrary code execution from the renderer
+contextBridge.exposeInMainWorld('require', require);
+```
+
+### ❌ Never use `nodeIntegration: true`
+
+```typescript
+// BAD — completely disables the security boundary
+new BrowserWindow({
+ webPreferences: { nodeIntegration: true, contextIsolation: false }
+});
+```
+
+### ❌ Never pass unsanitized IPC data to shell commands
+
+```typescript
+// BAD — command injection vulnerability
+ipcMain.handle('run-cmd', (_event, cmd: string) => {
+ exec(cmd); // Attacker can run ANY command
+});
+```
+
+### ✅ Always validate IPC arguments in the main process
+
+```typescript
+// GOOD — validate and constrain inputs
+ipcMain.handle('file:read', async (_event, filename: string) => {
+ // Validate: only allow specific filenames, no path separators
+ if (filename.includes('/') || filename.includes('\\')) {
+ throw new Error('Invalid filename');
+ }
+ const safePath = join(app.getPath('userData'), 'data', filename);
+ return readFileSync(safePath, 'utf-8');
+});
+```
+
+### ✅ Always return unsubscribe functions for event listeners
+
+```typescript
+// GOOD — prevents memory leaks in React's useEffect
+onSomeEvent: createEventSubscription('channel:event')
+
+// In React:
+useEffect(() => {
+ const unsub = window.electronAPI.onSomeEvent((data) => { /* ... */ });
+ return () => unsub(); // Cleanup on unmount
+}, []);
+```