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: falsePer-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: alwaysShips 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=ABCDE12345electron-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-updatesPoint 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 testFor 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:bundleVITE_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.
