meta

Building Production Mac Apps with Electron & Angular: Complete Desktop Deployment Guide

Master Mac app distribution with Electron and Angular. Learn code signing, notarization, DMG creation, Mac App Store submission, and direct distribution workflows from real production experience.

Vatsal Shah
Building Production Mac Apps with Electron & Angular: Complete Desktop Deployment Guide

Introduction

Building a production-ready Mac desktop application using Electron and Angular requires navigating code signing, notarization, certificate management, and distribution workflows that are often undocumented or scattered across multiple sources. This comprehensive guide covers everything you need to know about deploying Angular desktop apps on macOS, from Electron setup to Mac App Store distribution. Whether you're converting a web app to desktop, building cross-platform applications, or publishing Mac apps, this production-tested approach will help you master Electron Mac deployment, code signing workflows, and notarization processes.

TL;DR: Electron enables rapid desktop app development with web technologies, but Mac distribution requires mastering code signing, notarization, and DMG creation. The complexity lies in Apple's security requirements and certificate management—not in Electron itself.

Related Reading: Explore our Mobile Deployment Guide for complementary insights.


Why Electron for Mac Desktop Apps

ApproachDevelopment SpeedNative FeaturesBundle SizeMaintenance
ElectronVery FastFull AccessLarger (~100MB+)Single Codebase
Native (Swift)SlowFull AccessSmaller (~20MB)Separate Codebase
Electron (Optimized)FastFull AccessMedium (~60MB)Single Codebase

Electron's Advantages for Mac Apps:

  1. Web Technology Stack – Leverage existing Angular/React/Vue skills for desktop app development
  2. Cross-Platform Desktop Apps – Same codebase works on Mac, Windows, and Linux platforms
  3. Native macOS APIs – Full access to macOS system features and native desktop functionality
  4. Rapid Desktop Development – Faster iteration than native Swift development for Mac applications
  5. Rich npm Ecosystem – Extensive package ecosystem available for desktop app features and integrations

Architecture Overview

Production Electron Mac applications use this architecture pattern:

Key Components:

  1. Main Process (electron/main.ts) – Controls app lifecycle and native APIs
  2. Renderer Process – Angular application running in BrowserWindow
  3. IPC Bridge – Secure communication between main and renderer
  4. Electron Forge – Build and packaging system
  5. Notarization Service – Apple's security verification

Tech Stack & Configuration

Core Framework Stack

Desktop Framework:

  • Electron 38.1.0 – Latest stable desktop app framework with enhanced security for Mac applications
  • Electron Forge 7.9.0 – Modern build and packaging system for streamlined Mac app distribution
  • Angular 17.3.12 – Web application framework optimized for desktop app development
  • TypeScript 5.4 – Type safety across Electron and Angular for reliable desktop application development

Build Tools:

  • Electron Forge Makers – DMG, ZIP, NSIS installers for cross-platform desktop app distribution
  • @electron/notarize – Apple notarization integration for Mac App Store compliance
  • appdmg – DMG creation with custom layouts for professional Mac app installers

Electron Forge Configuration

⚙️ Click to view Electron Forge Production Configuration
// forge.config.js - Complete Production Configuration
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
const packageJson = require('./package.json');

module.exports = {
  packagerConfig: {
    asar: true,
    // Code signing configuration for macOS
    osxSign: process.env.MAS_BUILD
      ? {
          // App Store configuration
          identity: 'Apple Distribution: Your Company (TEAM_ID)',
          'hardened-runtime': true,
          'gatekeeper-assess': false,
          entitlements: 'build/mac/entitlements.mas.plist',
          'entitlements-inherit': 'build/mac/entitlements.mas.inherit.plist',
          'signature-flags': 'library',
          'pre-embed-provisioning-profile': 'build/mac/YourApp_Mac_App_Store.provisionprofile',
        }
      : {
          // Direct distribution configuration
          identity:
            process.env.APPLE_SIGNING_IDENTITY ||
            'Developer ID Application: Your Company (TEAM_ID)',
          'hardened-runtime': true,
          'gatekeeper-assess': false,
          entitlements: 'build/mac/entitlements.mac.plist',
          'entitlements-inherit': 'build/mac/entitlements.mac.plist',
          'signature-flags': 'library',
          'pre-embed-provisioning-profile': false,
        },
    appBundleId: 'com.yourcompany.yourapp',
    appCategoryType: 'public.app-category.productivity',
    icon: 'src/assets/media/logos/icons/mac/icon.icns',
    name: 'YourApp',
    executableName: 'YourApp',
    extendInfo: {
      CFBundleIdentifier: 'com.yourcompany.yourapp',
      CFBundleName: 'YourApp',
      CFBundleDisplayName: 'YourApp',
      CFBundleExecutable: 'YourApp',
      CFBundleShortVersionString: packageJson.version,
      CFBundleVersion: '1',
      CFBundleDevelopmentRegion: 'en',
      CFBundlePackageType: 'APPL',
      CFBundleInfoDictionaryVersion: '6.0',
    },
  },
  plugins: [
    {
      name: '@electron-forge/plugin-auto-unpack-natives',
      config: {},
    },
    new FusesPlugin({
      version: FuseVersion.V1,
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    }),
  ],
};

Configuration Highlights:

  • Conditional Signing – MAS_BUILD env var switches between App Store and direct distribution
  • Hardened Runtime – Required for notarization
  • Security Fuses – Disable Node.js injection and enable integrity validation
  • ASAR Packaging – Archive format for app resources

Electron Main Process

Main Process Implementation

⚡ Click to view Electron Main Process Implementation
// electron/main.ts - Production Main Process
import { app, BrowserWindow, dialog, ipcMain, safeStorage } from 'electron';
import * as path from 'path';
import * as url from 'url';
import { createMenu } from './menu';

let mainWindow: BrowserWindow | null;

function createWindow() {
  const windowOptions: any = {
    width: 1200,
    height: 800,
    title: 'YourApp',
    backgroundColor: '#FFFFFF',
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      webSecurity: true,
      devTools: process.env.NODE_ENV === 'development',
      preload: path.join(__dirname, 'preload.js'),
      experimentalFeatures: true,
    },
  };

  mainWindow = new BrowserWindow(windowOptions);

  // Set custom headers for platform identification
  mainWindow.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
    const platform = 'mac';
    const appOrigin =
      process.env.NODE_ENV === 'development'
        ? `https://yourdomain.com/dev-${platform}`
        : `https://yourdomain.com/${platform}`;

    details.requestHeaders['x-app-origin'] = appOrigin;
    details.requestHeaders['x-app-platform'] = platform;
    details.requestHeaders['x-app-version'] = app.getVersion();
    callback({ requestHeaders: details.requestHeaders });
  });

  // Handle permission requests
  mainWindow.webContents.session.setPermissionRequestHandler(
    (webContents, permission, callback) => {
      // Allow microphone, camera, notifications, file system
      if (['media', 'display-capture', 'notifications', 'fileSystem'].includes(permission)) {
        callback(true);
        return;
      }
      callback(false);
    },
  );

  // Load application
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:8000');
    mainWindow.webContents.openDevTools();
  } else {
    const indexPath = path.join(app.getAppPath(), 'dist/yourapp/index.html');
    mainWindow.loadURL(
      url.format({
        pathname: indexPath,
        protocol: 'file:',
        slashes: true,
      }),
    );
  }

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.setName('YourApp');
app.setAppUserModelId('com.yourcompany.yourapp');

app.whenReady().then(() => {
  createWindow();
});

app.on('activate', () => {
  if (mainWindow === null) {
    createWindow();
  }
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

Security Best Practices:

  • Context Isolation – Prevents renderer from accessing Node.js
  • Node Integration Disabled – Renderer runs in secure sandbox
  • Web Security Enabled – Enforces CORS and content security
  • Preload Script – Controlled bridge between main and renderer

Code Signing Setup

Certificate Types

Certificate TypeUse CaseIdentity Format
Developer ID ApplicationDirect DistributionDeveloper ID Application: Your Company
Apple DistributionMac App StoreApple Distribution: Your Company
Apple DevelopmentDevelopment/TestingApple Development: Your Name

Certificate Installation

# List available signing identities
security find-identity -v -p codesigning

# Export certificate for backup
security export -k ~/Library/Keychains/login.keychain-db \
  -t identities -f pkcs12 -o ./certificate.p12 \
  "Developer ID Application: Your Company (TEAM_ID)"

Certificate Requirements:

  • Valid Apple Developer Program membership ($99/year)
  • Certificates downloaded from Apple Developer Portal
  • Private keys stored securely in Keychain
  • Team ID matching provisioning profiles

Notarization Workflow

Notarization Script

🔐 Click to view Notarization Script Implementation
// electron/notarize.js - Production Notarization
require('dotenv').config();
const { notarize } = require('@electron/notarize');

exports.default = async function notarizing(context) {
  console.log(`🔐 Starting notarization process...`);
  const { electronPlatformName, appOutDir } = context;

  if (electronPlatformName !== 'darwin') {
    console.log(`ℹ️  Notarization not required for platform: ${electronPlatformName}`);
    return;
  }

  const appName = context.packager.appInfo.productFilename;
  const appPath = `${appOutDir}/${appName}.app`;

  // Verify environment variables
  if (
    !process.env.APPLE_TEAM_ID ||
    !process.env.APPLE_ID ||
    !process.env.APPLE_APP_SPECIFIC_PASSWORD
  ) {
    throw new Error('Missing required Apple notarization environment variables.');
  }

  const data = {
    tool: 'notarytool', // Use modern notarytool (not deprecated altool)
    teamId: process.env.APPLE_TEAM_ID,
    appBundleId: 'com.yourcompany.yourapp',
    appPath: appPath,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
  };

  console.log('⏳ Submitting to Apple for notarization...');
  console.log('   This may take 5-15 minutes...');

  await notarize(data);

  console.log('✅ Notarization complete!');
  console.log('🎉 Your app is now ready for distribution!');
};

Notarization Requirements:

  • Hardened Runtime – Enabled in entitlements
  • Code Signing – App and all nested code signed
  • Valid Certificates – Developer ID or Apple Distribution
  • App-Specific Password – Generated in Apple ID account settings

Environment Variables:

# .env file (never commit)
APPLE_TEAM_ID=YOUR_TEAM_ID
APPLE_ID=your-email@example.com
APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx

DMG Creation Process

DMG Creation Script

📦 Click to view DMG Creation Script
// scripts/create-dmg.js - Custom DMG Builder
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

console.log('🎯 Creating DMG file...');

const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const version = packageJson.version;

// Verify app bundle exists
const appPath = 'out/YourApp-darwin-arm64/YourApp.app';
if (!fs.existsSync(appPath)) {
  console.error('❌ App bundle not found. Please run electron:package first.');
  process.exit(1);
}

// Remove existing DMG
const dmgPath = 'out/YourApp.dmg';
if (fs.existsSync(dmgPath)) {
  console.log('🗑️  Removing existing DMG...');
  fs.unlinkSync(dmgPath);
}

// Create DMG configuration
const dmgConfig = {
  title: 'YourApp',
  icon: path.resolve('src/assets/media/logos/icons/mac/icon.icns'),
  background: path.resolve('src/assets/media/logos/icons/png/256x256.png'),
  contents: [
    { x: 410, y: 150, type: 'link', path: '/Applications' },
    { x: 130, y: 150, type: 'file', path: path.resolve(appPath) },
  ],
  window: {
    width: 540,
    height: 380,
  },
};

// Write config and create DMG
const configPath = 'out/dmg-config.json';
fs.writeFileSync(configPath, JSON.stringify(dmgConfig, null, 2));

try {
  console.log('📦 Building DMG...');
  execSync(`cd out/YourApp-darwin-arm64 && npx appdmg ../dmg-config.json ../YourApp.dmg`, {
    stdio: 'inherit',
  });

  // Sign the DMG
  console.log('🔐 Signing DMG...');
  const signingIdentity =
    process.env.APPLE_SIGNING_IDENTITY ||
    'Developer ID Application: Your Company (TEAM_ID)';
  execSync(`codesign --sign "${signingIdentity}" --timestamp --options runtime out/YourApp.dmg`, {
    stdio: 'inherit',
  });

  // Verify signature
  console.log('🔍 Verifying DMG signature...');
  execSync('codesign --verify --verbose out/YourApp.dmg', {
    stdio: 'inherit',
  });

  console.log('✅ DMG created and signed successfully: out/YourApp.dmg');
} catch (error) {
  console.error('❌ Failed to create/sign DMG:', error.message);
  process.exit(1);
}

DMG Features:

  • Custom Layout – App icon and Applications folder link
  • Background Image – Branded installer experience
  • Code Signing – DMG itself is signed for security
  • Stapling – Notarization ticket attached to DMG

Build Pipeline

Build Workflow:

  1. Clean – Remove previous builds
  2. Version Update – Increment version numbers
  3. Angular Build – Production-optimized web assets with base-href for file:// protocol
  4. TypeScript Compile – Electron main process compilation
  5. Package – Create app bundle using Electron Forge
  6. Make – Generate installers (DMG, ZIP, etc.)
  7. Notarize – Apple security verification (automated)
  8. DMG Creation – Custom installer creation with branding

Distribution Methods

Direct DMG Distribution

Advantages:

  • ✅ Faster distribution (no app store review)
  • ✅ Full control over pricing and updates
  • ✅ Can distribute outside Mac App Store
  • ✅ No revenue sharing (30% fee)

Requirements:

  • ✅ Developer ID certificate
  • ✅ Notarization (required for macOS 10.15+)
  • ✅ Signed DMG file

Mac App Store Distribution

Advantages:

  • ✅ Discoverability through App Store
  • ✅ Automatic updates via App Store
  • ✅ User trust and security verification
  • ✅ Sandboxed execution environment

Requirements:

  • ✅ Apple Distribution certificate
  • ✅ Provisioning profile
  • ✅ App Store Connect account
  • ✅ App Store review approval

Security & Entitlements

Entitlements Configuration

🔑 Click to view Mac Entitlements Configuration
<!-- build/mac/entitlements.mac.plist - Direct Distribution -->
<?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">
<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/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.device.audio-input</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
  </dict>
</plist>

Common Entitlements:

  • JIT Compilation – Required for V8 JavaScript engine
  • Audio Input – Microphone access
  • Camera – Webcam access
  • Network Client – Internet connectivity
  • File Access – User-selected file read/write

Hardened Runtime

Required for Notarization:

  • Prevents code injection
  • Restricts library loading
  • Enforces signature validation
  • Controls system resource access

Common Challenges & Solutions

Challenge 1: Notarization Failures

Problem: App rejected during notarization

Common Causes:

  • Missing hardened runtime
  • Unsigned nested code
  • Invalid entitlements
  • Expired certificates

Solution:

# Verify app signature
codesign -dv --verbose=4 YourApp.app

# Check for unsigned libraries
find YourApp.app -type f -exec codesign -v {} \;

# Review notarization logs
xcrun notarytool history --apple-id "your-email" --password "app-specific-password" --team-id "TEAM_ID"

Challenge 2: DMG Not Opening

Problem: "YourApp.dmg is damaged" error

Solution:

# Remove quarantine attribute
xattr -cr YourApp.dmg

# Verify signature
codesign --verify --verbose YourApp.dmg

# Re-sign if needed
codesign --sign "Developer ID Application: Your Company" --timestamp YourApp.dmg

Challenge 3: Code Signing Errors

Problem: Certificate chain issues

Solution:

# Install WWDR intermediate certificate
curl -O https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer
sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" AppleWWDRCAG3.cer

# Verify certificate chain
security verify-cert -c certificate.p12

Challenge 4: Large Bundle Size

Problem: App bundle too large (>150MB), affecting download times and user experience

Solution:

  • Enable ASAR packaging for compressed app archives (already configured in Electron Forge)
  • Exclude unnecessary files in forge.config.js using ignore patterns
  • Tree-shake unused dependencies during Angular build process
  • Optimize images and assets using WebP format and appropriate compression
  • Use Electron's built-in optimization flags and code splitting for desktop applications
  • Consider lazy-loading modules to reduce initial bundle size for Mac apps

Performance Optimization

Bundle Size Optimization

📉 Click to view Bundle Size Optimization Configuration
// forge.config.js - File Exclusion
ignore: [
  /^\/src\/(?!assets\/media\/logos\/icons\/(mac|win|png)\/)/,
  /^\/\.angular/,
  /^\/\.git/,
  /^\/ios/,
  /^\/android/,
  /^\/docs/,
  /^\/build/,
  /^\/release/,
  /^\/out/,
  /^\/logs/,
]

Optimization Results:

OptimizationBeforeAfterSavings
ASAR Packaging120MB95MB21%
File Exclusion95MB78MB18%
Image Optimization78MB65MB17%
Total120MB65MB46%

Deployment Metrics

Build Performance

MetricTime
Angular Build45s
Electron Package30s
Code Signing15s
Notarization8-15 min
DMG Creation10s
Total~20 min

Distribution Statistics

  • DMG Size: 65MB (compressed from 120MB)
  • Installation Time: Less than 30 seconds
  • User Satisfaction: High ratings typical for well-built Electron apps
  • Crash Rate: Less than 0.1%

Real-World Insights

What Worked Well

  1. Electron Forge – Modern, well-maintained build system
  2. Automated Notarization – Integrated into build pipeline
  3. Custom DMG Layout – Professional installer experience
  4. TypeScript – Type safety across Electron and Angular
  5. Security Fuses – Enhanced app security

Lessons Learned

  1. Start with Certificates Early – Can take days to obtain
  2. Test Notarization Locally – Don't wait until production
  3. Automate Everything – Manual steps lead to errors
  4. Monitor Notarization Status – Check logs if failures occur
  5. Keep Certificates Secure – Back up but don't commit to git

Common Mistakes to Avoid

  • ❌ Committing certificates or passwords to git
  • ❌ Skipping notarization (required for macOS 10.15+)
  • ❌ Not testing on fresh macOS installations
  • ❌ Using deprecated altool instead of notarytool
  • ❌ Missing hardened runtime entitlements

Conclusion

Building production Mac applications with Electron and Angular is entirely feasible and offers significant advantages in development speed and code reuse. The complexity lies in mastering Apple's security requirements—code signing, notarization, and certificate management—not in the Electron framework itself.

Key Takeaways

  • Electron enables rapid desktop development with web technologies, making Mac app development accessible to web developers
  • Code signing and notarization are required for modern macOS and essential for Mac App Store distribution
  • Automation is critical – Manual processes lead to errors in desktop app deployment workflows
  • Certificate management requires careful planning and secure storage for Mac app distribution
  • Security fuses enhance app protection against tampering in Electron desktop applications
  • DMG distribution provides flexible Mac app deployment options outside the App Store

Next Steps

  1. Set up Electron development environment
  2. Obtain Apple Developer certificates
  3. Configure build pipeline with automation
  4. Test notarization process thoroughly
  5. Create distribution workflows for both DMG and App Store

Further Reading


Frequently Asked Questions

Tags

electronangularmacmacosdesktop appcode signingnotarizationdmgmac app storedistribution

Related Articles