- 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.
20 KiB
Desktop Auto-Update System
apps/desktop utilizes a unified update lifecycle powered by electron-updater. This architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
๐ Reactive Update Flow
The following diagram illustrates the Reactive Update Flow, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
graph TD
%% โโโ Styling Definitions (Dark-Mode Friendly Enterprise Palette) โโโ
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
classDef ipcBridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
%% โโโ Subgraphs โโโ
subgraph MainProcess ["Main Process (src/main/index.ts)"]
AUTO[autoUpdater.checkForUpdatesAndNotify]
EVENTS{{Update Events: progress, downloaded, error}}
SENDER[sendToRenderer]
end
subgraph Preload ["IPC Bridge (src/preload/index.ts)"]
EXPOSE{contextBridge.exposeInMainWorld}
end
subgraph Renderer ["Renderer (React App - apps/web)"]
HOOK([useElectronUpdater Hook])
ACTIONS[UI Actions: Install, Check]
end
%% โโโ Flow & Relationships โโโ
AUTO ---> EVENTS
EVENTS ---> SENDER
SENDER ===>|'updater:*' Event Stream| EXPOSE
EXPOSE ===>|electronAPI window object| HOOK
HOOK -.->|Reactive State Status and Progress| ACTIONS
ACTIONS -.->|ipcRenderer.invoke| EXPOSE
EXPOSE -.->|Trigger Update or Install| AUTO
%% โโโ Apply Styles โโโ
class AUTO,EVENTS,SENDER coreEntity;
class EXPOSE ipcBridge;
class HOOK,ACTIONS appEntity;
%% โโโ Subgraph Backgrounds (Transparent for Native GitHub Support) โโโ
style MainProcess fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
style Renderer fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
Lifecycle Sequence
- App launch โ After a 3-second initialization delay,
autoUpdater.checkForUpdatesAndNotify()is invoked. - Update detected โ If
autoDownloadistrue(default), the binary payload downloads in the background. - Progress streaming โ
download-progressevents are forwarded to the renderer via IPC in real-time. - Download complete โ The renderer surfaces a "Restart to Update" prompt to the user.
- User-initiated install โ
autoUpdater.quitAndInstall()terminates the current process and launches the updated binary.
๐ Current Provider: GitHub Releases
The update provider is declared in electron-builder.yml:
publish:
provider: github
owner: YOUR_GITHUB_ORG
repo: YOUR_REPO_NAME
Operational Mechanics
-
When
electron-builder --publish alwaysexecutes, it:- Compiles the application for the target platform.
- Uploads the installer(s) to a GitHub Release tagged with the version from
package.json. - Generates and uploads the platform-specific manifest:
latest.yml(Windows),latest-mac.yml(macOS), orlatest-linux.yml(Linux).
-
When the packaged application calls
checkForUpdates(),electron-updater:- Reads
app-update.ymlfrom the app'sresources/directory (auto-generated during build โ never manually created). - Fetches the appropriate
latest*.ymlmanifest from the configured release endpoint. - Performs a semantic version comparison and initiates the download if a newer version exists.
- Reads
๐ Release Workflow: The Deterministic Pipeline
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
Manual Release
# 1. Version bump โ semver discipline
cd apps/desktop
npm version patch # or: minor, major
# 2. Compile web assets
cd ../..
pnpm build --filter=web
# 3. Synchronize, compile, and publish
cd apps/desktop
pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
[!CAUTION] > Treat
GH_TOKENas a critical secret. It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
Automated Release (GitHub Actions)
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. Authorizes release artifact uploads. |
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 | Passphrase for the .p12 certificate. |
APPLE_ID |
macOS only | macOS | Apple ID email for notarization submission. |
APPLE_APP_SPECIFIC_PASSWORD |
macOS only | macOS | App-specific password generated at 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 | Passphrase for the Windows certificate. |
Configuring Secrets
- Navigate to Settings โ Secrets and variables โ Actions in your GitHub repository.
- Add each variable as a Repository secret.
- Reference them in workflow files as
${{ secrets.VARIABLE_NAME }}.
โ๏ธ Deployment Strategies
AWS S3 (Private Infrastructure)
For enterprise environments requiring private infrastructure, the system can be reconfigured to target an AWS S3 Bucket or a CloudFront Distribution.
Update electron-builder.yml:
publish:
provider: s3
bucket: your-bucket-name
region: ap-southeast-1
path: /desktop-releases
acl: private
Additional environment variables:
| Variable | Description |
|---|---|
AWS_ACCESS_KEY_ID |
IAM access key with S3 PutObject and GetObject permissions |
AWS_SECRET_ACCESS_KEY |
IAM secret key |
Bucket structure:
your-bucket/desktop-releases/
โโโ latest.yml (Windows manifest)
โโโ latest-mac.yml (macOS manifest)
โโโ latest-linux.yml (Linux manifest)
โโโ EigenDesktop-Setup-0.2.0.exe
โโโ EigenDesktop-0.2.0.dmg
โโโ EigenDesktop-0.2.0-mac.zip
โโโ EigenDesktop-0.2.0.AppImage
Note
The bucket must allow public read access to the manifest files (
latest*.yml), or you must configure a CloudFront distribution.electron-updaterperforms unauthenticatedGETrequests to resolve the latest version.
Generic File Server (Self-Hosted)
For self-hosted infrastructure (Nginx, Caddy, etc.):
publish:
provider: generic
url: [https://updates.your-domain.com/desktop](https://updates.your-domain.com/desktop)
Your server must host the same directory structure as the S3 layout above.
[!IMPORTANT] > MIME Type Configuration: Ensure your file server correctly serves
.ymlfiles withtext/yamland installer binaries withapplication/octet-stream. Incorrect MIME types will cause download corruption or silent update failures.
Nginx reference:
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";
# MIME types for update manifests
types {
text/yaml yml;
application/octet-stream exe dmg AppImage zip;
}
}
}
๐ก๏ธ Code Signing: The Trust Boundary
[!WARNING] > Code signing is not merely a requirement โ it is the Trust Boundary established by the operating system. macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures,
electron-updaterwill reject update payloads entirely.
macOS
- Requires an Apple Developer ID Application certificate ($99/year Apple Developer Program).
- The
electron-builder.ymlis configured with: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 version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "[http://www.apple.com/DTDs/PropertyList-1.0.dtd](http://www.apple.com/DTDs/PropertyList-1.0.dtd)"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-jit</key> <true/> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> <key>com.apple.security.cs.allow-dyld-environment-variables</key> <true/> </dict> </plist> - Notarization is mandatory for macOS 10.15+. Provide
APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD, andAPPLE_TEAM_ID.
Windows
- Requires an EV Code Signing Certificate or a standard code signing certificate from a trusted CA.
- Without signing, Windows SmartScreen warns users with "Windows protected your PC" โ severely impacting adoption.
- EV certificates eliminate SmartScreen warnings immediately; standard certificates build trust reputation over time through Microsoft's telemetry.
Linux
- Code signing is not enforced by the OS for AppImage distribution.
- Optional GPG signing is available for package managers that support it.
๐งช Testing Updates in Development
Note
The auto-updater is intentionally disabled in development mode to prevent runtime crashes. Setting
forceDevUpdateConfigrequires adev-app-update.ymlfile, which introduces unnecessary complexity during local development.
What Happens in Dev Mode
In src/main/index.ts, the setupAutoUpdaterEvents() function detects IS_DEV and returns early:
if (IS_DEV) {
autoUpdater.autoDownload = false;
return; // Skip event registration โ no update server in dev
}
This means:
- No update check is performed on startup.
- No
electron-updaterevents are emitted. - The
useElectronUpdater()hook will remain inidlestatus.
How to Test Updates
Auto-update can only be fully validated using a packaged, signed build distributed through a real update channel:
- Publish v0.1.0 โ Package and release a signed build.
- Bump to v0.2.0 โ Increment the version in
package.json. - Publish v0.2.0 โ Package and release the updated build.
- Launch v0.1.0 โ The app should detect v0.2.0, download it, and prompt the user to restart.
For rapid iteration, use the generic provider pointing to a local Nginx or Python HTTP server:
# electron-builder.yml (temporary, for testing)
publish:
provider: generic
url: http://localhost:8080/updates
# Serve the release directory locally
cd apps/desktop/release
python3 -m http.server 8080 --directory .
โ ๏ธ Diagnostic Runbook
Issue: "Update check failed" on startup
| Symptom | Root Cause | Resolution |
|---|---|---|
Console logs [AutoUpdater] Startup check failed (possibly offline) |
The machine is offline, or the update server (GitHub/S3/generic) is unreachable. | No action required. The error is caught in a try/catch block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
Issue: app-update.yml not found in production build
| Symptom | Root Cause | Resolution |
|---|---|---|
electron-updater throws "Cannot find app-update.yml" immediately after launch. |
The publish block in electron-builder.yml is missing or misconfigured. electron-builder generates app-update.yml only when a valid provider is declared. |
Verify the publish block exists in electron-builder.yml. Run electron-builder --publish never and inspect release/*/resources/app-update.yml to confirm generation. |
Issue: "Cannot update: code signature is invalid" (macOS)
| Symptom | Root Cause | Resolution |
|---|---|---|
| The updater downloads a new version but refuses to apply it, logging a signature validation error. | The application was not signed, or the signing certificate has expired / been revoked. | Ensure CSC_LINK and CSC_KEY_PASSWORD are correctly set in CI. Verify the packaged app with: codesign --verify --deep --strict release/mac*/Desktop.app. Re-sign and re-publish if the certificate was rotated. |
Issue: Updates work on Windows/Linux but not macOS
| Symptom | Root Cause | Resolution |
|---|---|---|
| Windows and Linux users receive updates, but macOS users see no update prompt. | macOS requires both a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. | Provide all Apple credential environment variables (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID) and ensure hardenedRuntime: true is set in electron-builder.yml. Re-package and re-publish. |
Issue: S3/Generic provider returns corrupted downloads
| Symptom | Root Cause | Resolution |
|---|---|---|
| Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale latest*.yml files. |
Verify MIME types: .yml โ text/yaml, .exe/.dmg/.AppImage/.zip โ application/octet-stream. Add Cache-Control: no-cache headers to latest*.yml responses. Invalidate CDN cache after publishing a new release. |