Packaging

Build installers for macOS, Windows, and Linux with electron-builder.

"Deployment" for a desktop app means producing installers and (optionally) hosting an update feed — there's no web host. This page covers building signed installers per platform with electron-builder; the feed is on Auto Update.

The build pipeline

Each platform build is two steps, wired together by the package.json scripts: electron-vite build compiles the three bundles into out/, then electron-builder packages them into installers under dist/.

pnpm build:mac      # run-s vite:build:app electron:build:mac
pnpm build:win      # → NSIS installer
pnpm build:linux    # → AppImage + snap + deb
pnpm build:unpack   # vite build + electron-builder --dir (fast, no installer)

Use pnpm build:unpack for a quick local sanity check — it produces an unpacked app directory without building an installer, so it's much faster while iterating.

You can only build a platform's installer on that platform (or a suitable CI runner). macOS builds need macOS; the Linux snap target needs Linux. Don't expect pnpm build:win to produce a working signed installer from macOS.

electron-builder.yml

The config is small and declarative. Key fields:

appId: com.soarstarter.electron      # from shared/constants APP_ID
productName: soar-electron           # PRODUCT_SLUG
asar: true                           # app packed into an archive
asarUnpack: [resources/**]           # resources stay on disk (icons, hooks)
protocols: [{ name: Soar Electron, schemes: [soar-electron] }]
files: [out/**, resources/**, package.json]
npmRebuild: false

Per-platform targets:

mac:
  hardenedRuntime: true
  gatekeeperAssess: false
  entitlements: resources/build/entitlements.mac.plist
  extendInfo:
    - NSCameraUsageDescription: ...
    - NSMicrophoneUsageDescription: ...
    - NSDocumentsFolderUsageDescription: ...
    - NSDownloadsFolderUsageDescription: ...
  notarize: false          # the afterSign hook notarizes instead
dmg:
  artifactName: ${name}-${version}.${ext}

Ships as a DMG. Hardened runtime + entitlements are prerequisites for notarization.

win:
  executableName: soar-electron
nsis:
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  createDesktopShortcut: always

Ships as an NSIS installer.

linux:
  target: [AppImage, snap, deb]
  maintainer: Soar Starter
  category: Utility
appImage:
  artifactName: ${name}-${version}.${ext}

Ships AppImage + snap + deb. Trim the target list to only what you distribute.

Code signing

Signing is env-driven and skipped when unset, so a credential-less build always succeeds (unsigned). Provide the secrets — typically as CI secrets — to produce signed artifacts.

Signing uses your Developer ID certificate from the login keychain. Notarization runs from the env-driven afterSign: resources/build/notarize.cjs hook, which reads APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID and no-ops when they're unset. electron-builder's built-in notarize is left false by design so credential-less builds don't fail.

APPLE_ID=you@example.com
APPLE_APP_SPECIFIC_PASSWORD=abcd-efgh-ijkl-mnop
APPLE_TEAM_ID=ABCDE12345

electron-builder signs the installer only when CSC_LINK (a PFX/P12 path or base64 blob) and CSC_KEY_PASSWORD are present.

CSC_LINK=./cert.pfx
CSC_KEY_PASSWORD=••••••

Unsigned Windows builds run, but SmartScreen warns users until the certificate accrues download reputation — an EV certificate clears this instantly.

Linux packages are conventionally unsigned; there's nothing extra to configure.

Update feed URL

electron-builder.env holds the publish URL that publish.url reads at package time:

UPDATE_SERVER_URL=https://example.com/auto-updates

Point it at where you host the update feed before building a release you intend to ship. Details on Auto Update.

Icons & usage strings

electron-builder auto-detects resources/build/icon.{icns,ico,png} per platform — there's no explicit icon field. To rebrand the icon, edit the two source SVGs and regenerate every raster with the recipe in docs/APP_ICONS.md (uses macOS sips/iconutil + png-to-ico, no ImageMagick). See also Branding.

The macOS NS*UsageDescription strings in extendInfo declare why the app may request camera, microphone, Documents, and Downloads access. Trim the ones your app doesn't use — a usage string you never trigger is dead metadata (and note the renderer permission handler denies these by default; see Windows, Menus & Tray).

Quality gate before a build

Run the AGENTS.md gate before packaging anything:

pnpm lint:check && pnpm typecheck && pnpm test

For a release build, also run the packaged smoke test and bundle report:

pnpm test:smoke        # Playwright against the built app, VITE_E2E_BYPASS_AUTH=true
pnpm analyze:bundle

VITE_E2E_BYPASS_AUTH is a test-only flag that bypasses the auth gate for smoke tests. Never set it in a real build — see Environment Variables.

On this page