
Distributing Desktop Apps: Code Signing and Installers
You spent months building a desktop app. The code is solid, tests pass, the UX turned out great. Then you generate the installer and send it to the client for testing. They try to open the file and get: "Windows Defender SmartScreen prevented an unrecognized app from starting." Or, on macOS: "'YourApp.app' can't be opened because the developer cannot be verified."
This moment is avoidable. Code signing -- digitally signing your code -- is what differentiates an app that installs without alarms from an app that looks like malware to modern operating systems. It's not a distribution detail that can be left for later. It's a requirement for any app that will be installed by real users outside your development environment.
The process is different on each platform and involves costs and timelines. Understanding how it works before reaching the distribution phase saves weeks of delays and unpleasant surprises with clients.
Windows: EV Certificate and Authenticode
Windows uses the Authenticode standard for signed code verification. SmartScreen, Microsoft's reputation system, analyzes two factors: whether the code is signed, and the accumulated reputation of the certificate used.
There are two types of certificates for Windows:
OV (Organization Validation) Certificate: Verifies the organization exists, but SmartScreen will still show an "unknown publisher" warning on initial installs until the certificate accumulates enough reputation. For apps with few users, this can take months.
EV (Extended Validation) Certificate: Requires in-person or notarial identity verification, costs more (typically $300 to $500/year), but bypasses the SmartScreen reputation accumulation process immediately. For enterprise apps, EV is the correct path.
Providers like DigiCert, Sectigo, and GlobalSign issue EV certificates. The validation process takes from a few days to two weeks depending on the provider and how quickly you respond to documentation requests.
With electron-builder, the Windows signing configuration is:
{
"build": {
"win": {
"target": ["nsis", "portable"],
"certificateFile": "cert.pfx",
"certificatePassword": "${CERTIFICATE_PASSWORD}",
"signingHashAlgorithms": ["sha256"],
"signDlls": true
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "YourApp"
}
}
}
signDlls: true is important: without it, only the main executable is signed, and unsigned DLLs inside the installer can trigger antivirus alerts.
The certificate password should never be in the code or repository. It must be an environment variable injected by CI/CD -- GitHub Actions Secrets, GitLab CI Variables, or equivalent.
macOS: Developer ID and Apple Notarization
macOS has a two-stage process: signing with a Developer ID certificate, and notarization -- submitting the signed app for automated analysis by Apple before distribution.
Gatekeeper, macOS's security system, has required notarization since Catalina (2019) for any app distributed outside the Mac App Store. Without notarization, macOS blocks the app with the "developer cannot be verified" message.
The Developer ID requires an Apple Developer Program account ($99/year). The certificate is generated via Xcode or Keychain Access and exported as a .p12 file for use in CI environments.
The electron-builder configuration for macOS:
{
"build": {
"mac": {
"target": ["dmg", "zip"],
"category": "public.app-category.productivity",
"identity": "Developer ID Application: Your Company (TEAM_ID)",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist",
"notarize": {
"teamId": "TEAM_ID"
}
}
}
}
hardenedRuntime: true is mandatory for notarization. The entitlements.mac.plist file declares the permissions the app needs -- network access, camera, microphone, etc. Apple checks whether the declared permissions make sense for the type of app.
The notarization process takes from a few minutes to a few hours. In a CI pipeline, this means the release build may take longer than development builds. Plan for this in your deploy SLA.
Linux: AppImage, .deb, and Snap
Linux is more permissive than Windows and macOS in terms of code verification -- there is no centralized system equivalent to SmartScreen or Gatekeeper. But the choice of distribution format directly impacts the installation experience.
| Format | Pros | Cons | Best for |
|---|---|---|---|
| AppImage | Portable, no installation needed | No native auto-update | Ad-hoc distribution |
| .deb | apt integration, familiar | Debian/Ubuntu only | Controlled environments |
| .rpm | dnf/yum integration | Red Hat/Fedora only | RHEL enterprise environments |
| Snap | Universal, sandboxed | Slower startup | Public distribution |
| Flatpak | Strong sandbox, universal | Complex initial setup | End-user apps |
For enterprise distribution -- where the company controls employee machines -- .deb or .rpm is the most practical choice because it allows automating installation via configuration management tools like Ansible or provisioning scripts.
For broad public distribution, AppImage has the advantage of running on virtually any Linux distribution without installation. Snap and Flatpak are alternatives with store distribution (Snap Store, Flathub) but add complexity to the publishing process.
CI/CD for Multi-Platform Builds
Builds for Windows, macOS, and Linux in parallel are feasible with GitHub Actions using platform-specific runners:
# .github/workflows/release.yml
name: Release Build
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Build and Sign (Windows)
if: matrix.os == 'windows-latest'
env:
CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
CSC_LINK: ${{ secrets.WINDOWS_CERT_BASE64 }}
run: npm run build:win
- name: Build and Notarize (macOS)
if: matrix.os == 'macos-latest'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.MACOS_CERT_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
run: npm run build:mac
- name: Build (Linux)
if: matrix.os == 'ubuntu-latest'
run: npm run build:linux
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}
path: dist/
The .p12 certificate is stored as base64 in a GitHub Secret (CSC_LINK), not as a file in the repository. electron-builder automatically decodes it when it finds the base64 value.
Conclusion
Code signing and notarization are investments that most projects discover they should have made from the start when they reach the first client demo and Windows blocks the installer. The process has costs -- between $100 and $500/year depending on the platforms -- and validation timelines that need to be factored into the project schedule.
The upside is that, with the right CI/CD configuration, the process becomes fully automated after initial setup. Every release generates signed and notarized installers for all platforms transparently.
At SystemForge, the build and distribution pipeline -- including code signing for all relevant platforms -- is part of the deliverable for every desktop app project. If you're planning to distribute a desktop app to clients and want to structure the distribution process professionally from the start, let's talk.
Need Desktop Software?
We build cross-platform desktop applications with Electron or Tauri.
Learn more →Need help?
