export(
appdir = "my-app",
destdir = "build",
sign = TRUE
)Signing tells the operating system who built this. Notarization tells the OS that Apple has seen it. Without either, macOS Gatekeeper blocks your app as an “unidentified developer” and Windows SmartScreen warns on every download. This guide covers what you need, what it costs, and how to wire it up. For the wider security picture, see Security Considerations.
When signing matters
Users can click past warnings, but the friction is real. Three reasons to sign anyway:
- Trust. Non-technical users read “unidentified developer” as “malware” and leave.
- Enterprise. Many IT policies block anything unsigned outright.
-
Auto-updates.
electron-updaterverifies signatures on macOS and Windows before applying an update, so unsigned builds lose the update path entirely.
The concrete outcomes differ per platform:
| macOS | Windows | Linux | |
|---|---|---|---|
| Required? | Strongly recommended | Strongly recommended | Optional |
| Cost | $99/year (Apple Developer) | $200 to $700/year (CA cert) | Free (GPG) |
| Certificate | Developer ID Application | OV or EV code signing | GPG key |
| Notarization | Yes (macOS 10.15+) | N/A | N/A |
Turning signing on
Pass sign = TRUE to export() and electron-builder does the rest:
sign = TRUE tells electron-builder to use whatever credentials it finds in the environment. It writes the signing configuration into package.json, sets the macOS identity, enables notarization (when configured), and points Windows at the certificate. It does not create certificates. If the credentials are missing, it warns and hands off to electron-builder, which then fails at the signing step with a specific error.
The function argument overrides the config file. export(..., sign = TRUE) signs even when _shinyelectron.yml says sign: false. You can also enable signing purely through the config:
signing:
sign: true
mac:
identity: "Developer ID Application: Your Name (TEAMID)"
team_id: "XXXXXXXXXX"
notarize: trueCredentials themselves (certificate password, Apple ID password, etc.) must stay in environment variables. Never commit them to the config file.
Before you kick off a full build, verify your setup with app_check():
app_check("my-app", sign = TRUE)That catches missing certificates or environment variables cheaply, without going through the full build.
macOS: sign and notarize
Cost: $99/year for Apple Developer Program membership, plus the time to generate an app-specific password.
What you need:
- An Apple Developer Program membership at developer.apple.com/programs.
- A Developer ID Application certificate. Not the same as a Mac App Store certificate. Create it in the Apple Developer portal under Certificates, Identifiers & Profiles.
- An app-specific password for notarization, generated at appleid.apple.com/account/manage.
Wiring it up. Export the Developer ID Application certificate from Keychain Access as a .p12 file, then set these environment variables:
# Certificate (path to .p12 file, or base64-encoded content)
export CSC_LINK="/path/to/certificate.p12"
export CSC_KEY_PASSWORD="your-certificate-password"
# Notarization credentials
export APPLE_ID="your@apple.id"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="XXXXXXXXXX"Then run export(..., sign = TRUE). electron-builder drives the full signing and notarization flow. You do not call codesign or xcrun notarytool yourself.
Without signing: Gatekeeper blocks the app on first launch with “App can’t be opened because Apple cannot check it for malicious software.” To open an unsigned .app during local development, strip the quarantine flag:
xattr -cr /path/to/YourApp.appOr right-click the app in Finder and choose Open to bypass Gatekeeper once.
Windows: sign with a code signing certificate
Cost: $200 to $700/year depending on validation level. An EV certificate builds SmartScreen trust immediately; an OV certificate builds reputation over several weeks, during which users still see warnings.
What you need: An OV or EV code signing certificate from a commercial CA. Common options:
EV certificates ship on a hardware token (USB dongle or HSM) and require a different signing workflow when used from CI; plan for that up front.
Wiring it up.
export CSC_LINK="/path/to/certificate.pfx"
export CSC_KEY_PASSWORD="your-certificate-password"Then:
export(
appdir = "my-app",
destdir = "build",
platform = "win",
sign = TRUE
)Without signing: unsigned Windows installers trigger SmartScreen on every download. There is no workaround short of signing the build.
Linux: optional GPG signing
Cost: free. Uses a GPG key you already control.
What you need: a GPG key whose private half lives on the build machine. Generate one with gpg --full-generate-key if you do not have one, and publish the public key where your users can find it (your website, a keyserver, or your GitHub profile).
Wiring it up.
export GPG_KEY="your-gpg-key-id"signing:
sign: true
linux:
gpg_sign: trueWithout signing: nothing visibly changes. AppImage files do not require a signature to run, so most users never notice. Sign if your audience includes security-minded users who want to verify authenticity with gpg --verify before installing.
Signing in CI
CI is where signing usually lives in the long run: no human holds the keys on a laptop, and every tagged release is signed automatically. Store credentials as GitHub Actions secrets (Settings → Secrets and variables → Actions) and reference them from your workflow:
- name: Build Electron app
env:
# macOS
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Windows (set in the Windows job instead)
# CSC_LINK: ${{ secrets.WIN_CERTIFICATE }}
# CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
run: |
Rscript -e "
shinyelectron::export(
appdir = 'app',
destdir = 'build',
sign = TRUE
)
"Important
macOS and Windows certificates use different files.
CSC_LINKandCSC_KEY_PASSWORDmust point at the right one for the runner. In a matrix build, set them per-platform in a conditionalenvblock or split the build into separate jobs so each runner gets only its own credentials.
For the full workflow template (toolchain setup, matrix jobs, artifact upload), see Building with GitHub Actions.
Development versus production
A working rhythm:
- Develop and iterate locally with
sign = FALSE(the default). Usexattr -cron macOS when you need to launch your own unsigned build. - Set up signing credentials once, per platform, in a secure vault or password manager.
- Flip
sign = TRUEfor release builds, either in theexport()call or throughsigning: sign: truein_shinyelectron.yml. - Move signing into CI so human laptops never touch the keys.
| Development | Production | |
|---|---|---|
sign |
FALSE (default) |
TRUE |
| Credentials needed? | No | Yes |
| Build speed | Fast | Slower: notarization takes 1 to 5 minutes |
| OS warnings? | Yes (use xattr -cr on macOS) |
No |
Environment variables reference
One canonical table for every variable the signing pipeline reads.
| Variable | Platform | Purpose |
|---|---|---|
CSC_LINK |
macOS, Windows | Path to .p12 or .pfx certificate (or base64-encoded content) |
CSC_KEY_PASSWORD |
macOS, Windows | Password for the .p12 or .pfx file |
APPLE_ID |
macOS | Apple ID email used for notarization |
APPLE_APP_SPECIFIC_PASSWORD |
macOS | App-specific password from appleid.apple.com
|
APPLE_TEAM_ID |
macOS | 10-character Apple Developer Team ID |
GPG_KEY |
Linux | GPG key ID for AppImage signing |
Next steps
- GitHub Actions: automate signed builds across platform runners.
- Auto Updates: electron-updater requires signed builds on macOS and Windows.
-
Configuration Guide: the full
_shinyelectron.ymlreference, signing included.