Packaging an Electron app for managed distribution across devices



A customer we were recently collaborating with came to us with an interesting problem regarding their cross platform codebase and managed distribution. We learned that they were planning to build an Electron-based application and needed help from Microsoft to understand how to build, deploy and manage the application on Windows using an MDM solution (eg. Microsoft Intune). Microsoft Intune is a cloud service that provides mobile device management and mobile application management capabilities. Together with the customer, Microsoft engineers built a sample electron application that could be opened in Kiosk mode and could be deployed through Microsoft Intune.

In this code story, we’ll explore the following aspects of our solution:

  • Packaging Electron source into Windows binaries
  • Setting the app in ‘Kiosk’ mode using PowerShell
  • Building a Configurable Windows installer (.MSI) using WiX Toolset
  • Cross-platform CI with AppVeyor and Travis

In order to focus on these aspects, we created a sample Electron app and iterated on it.

Windows Binaries

In perhaps the most straightforward part of our journey, we used the Electron Packager CLI tool to create OS-specific bundles from an Electron app.

electron-packager ./src --platform=win32 --arch=x64 --asar --overwrite --out ./dist

Kiosk Mode

Windows 10 Enterprise provides multiple ways to run an app in kiosk mode:

  • Assigned Access method, which allows a single Universal Windows Platform (UWP) app to run in kiosk mode.
  • Shell Launcher method, which allows a single classic Windows Application (e.g Electron app) to run in kiosk mode.

Intune provides a built-in Configuration Service Provider to remote-enable Assigned Access. It is as simple as providing a JSON with domain/local username and the UWP Application ID in  Azure Portal.


Since Electron is a classic Windows app, we chose Shell Launcher as our way forward.

Shell Launcher

Because Intune does not provide a direct way to remote-enable Shell Launcher, we had to use an elevated PowerShell script. The script takes the local/domain username as well as the full path of the Windows executable (.exe) produced in the previous step as parameters.

We turned on the Shell Launcher feature in Windows 10 (Programs and Features -> Turn Windows features on or off -> Expand ‘Device Lockdown’ -> Select Shell Launcher). Below is the code to do the same in PowerShell:

Enable-WindowsOptionalFeature -online -FeatureName Client-EmbeddedShellLauncher -all -NoRestart

Then we created a Shell Launcher Object as provided by Windows Management Instrumentation (WMI).

$ShellLauncherClass = [wmiclass]"\\localhost\root\standardcimv2\embedded:WESL_UserSetting"

In order to set a custom shell, we had to first find out the Security Identifier (SID) of the Windows username. We wrote a helper function for this:

function Get-UsernameSID($AccountName) {
        $NTUserObject = New-Object System.Security.Principal.NTAccount($AccountName)
        $NTUserSID = $NTUserObject.Translate([System.Security.Principal.SecurityIdentifier])
        return $NTUserSID.Value

Finally, we set the custom shell for the above SID and full application path:

# Last parameter is Default Action
# 0 - Restarts the shell application
# 1 - Restarts the device
# 2 - Shuts down the device
$ShellLauncherClass.SetCustomShell($Cashier_SID, $ExeName, ($null), ($null), 0)

For debugging purposes we also print modified shell settings:

$shellSetting = Get-WmiObject -namespace "root\standardcimv2\embedded" `
    -computer "localhost" -class WESL_UserSetting | Select-Object Shell, Sid

Write-Host $shellSetting

If ShellLauncher was successfully updated, the output of this command would be:

Shell                                                    Sid
-----                                                    ---
C:\kiosk-demo-electron-win32-x64\kiosk-demo-electron.exe S-1-5-21-XXXXXXXXXX-XXXXXXXXX-XXXXXXXXXX-XXXXXXX

Windows Auto-Logon

To provide a full kiosk experience, we decided to auto-login the kiosk user. For this step, we added another parameter $Password to the PowerShell Script and modify Windows Registry.

$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty $RegPath "AutoAdminLogon" -Value "1" -type String
Set-ItemProperty $RegPath "DefaultUsername" -Value "$UserName" -type String
Set-ItemProperty $RegPath "DefaultPassword" -Value "$Password" -type String

Windows Installer

For building Windows Installers for Electron apps, the most popular choice is the Squirrel-based electron-winstaller npm module. However, electron-winstaller does not allow custom parameters for the produced installer or executing elevated PowerShell; as a result, we chose to use  WiX Toolset (specifically, we chose the npm module wixtoolset-compiler). When executed normally, the installer prompts for Windows elevation; however, apps distributed with Intune are installed without interruption via the SYSTEM user, which keeps the elevation prompt silent.

WiX Toolset

Building a Windows installer with WiX is a multi-step process. The first step is to configure it with an XML file (.wxs). We have provided a base config file product.wxs that contains app name, version, author, a unique upgrade code, installer parameters with default values, as well as custom actions that execute our PowerShell script.

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="" xmlns:util="">
  <Product Id="*" Name="Kiosk Demo Electron" Language="1033" Version="" Manufacturer="Syed Hassaan Ahmed" UpgradeCode="a8d48e7b-86a0-4171-8db8-6fb3618dfdc6">
    <Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
    <MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="no" DowngradeErrorMessage="ok" AllowSameVersionUpgrades="yes" />
    <MediaTemplate EmbedCab="yes" />
    <!-- Params passed to PowerShell script -->
    <Property Id="KIOSK_USERNAME">kioskuser</Property>
    <Property Id="KIOSK_PASSWORD">kioskpassword</Property>
    <Property Id="EXE_NAME">kiosk-demo-electron.exe</Property>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="APPLICATIONROOTDIRECTORY" Name="Kiosk Demo Electron"/>
    <Feature Id="MainApplication" Title="Main Application" Level="1">
      <ComponentRef Id="InstallShellLauncher" />
      <ComponentGroupRef Id="ElectronBinaries" />
    <!-- Install Shell Launcher -->
    <CustomAction Id="InstallShellLauncher"
                  Value="&quot;powershell&quot; -NoProfile -NonInteractive -InputFormat None -ExecutionPolicy Bypass -File &quot;[APPLICATIONROOTDIRECTORY]Install-ShellLauncher.ps1&quot; &quot;[KIOSK_USERNAME]&quot; &quot;[KIOSK_PASSWORD]&quot; &quot;[APPLICATIONROOTDIRECTORY][EXE_NAME]&quot;"
    <CustomAction Id="InvokeInstall"
                  Impersonate="no" />
      <Custom Action="InstallShellLauncher" After="CostFinalize">NOT Installed</Custom>
      <Custom Action="InvokeInstall" After="InstallFiles">NOT Installed</Custom>

Then we execute WiX steps in this order:

1) Heat

Heat is a binary harvester that scans through folders or .NET solutions (.sln) for Windows binaries and generates an XML output with a list of files.

heat.exe dir "<PATH_TO_ELECTRON_BINARIES>" -v -ag -cg ElectronBinaries -var var.SourceDir -dr APPLICATIONROOTDIRECTORY -srd -suid -sfrag -sreg -out ./dist/heat.wxs

Here is a part of the generated heat.wxs output:

<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="">
            <Component Id="kiosk_demo_electron.exe" Guid="*">
                <File Id="kiosk_demo_electron.exe" KeyPath="yes" Source="$(var.SourceDir)\kiosk-demo-electron.exe" />
            <Component Id="node.dll" Guid="*">
                <File Id="node.dll" KeyPath="yes" Source="$(var.SourceDir)\node.dll" />
            <Component>...Other Chromium binaries...</Component>
        <ComponentGroup Id="ElectronBinaries">
            <ComponentRef Id="kiosk_demo_electron.exe" />
            <ComponentRef Id="node.dll" />
            <ComponentRef Id="...Other Chromium binaries..." />

2) Candle

Candle pre-processes .wxs files and generates compiled .wixobj files. We passed the above .wxs files to Candle.

candle.exe -v -ext WixUtilExtension -dSourceDir="<PATH_TO_ELECTRON_BINARIES>" ./tools/product.wxs ./dist/heat.wxs -out ./dist/

3) Light

The Light tool processes the previously mentioned .wixobj files and produces the final installer (.MSI).

light.exe -v -ext WixUtilExtension ./dist/product.wixobj ./dist/heat.wixobj -out ./dist/KioskDemoElectron.msi

The previous WiX steps were wrapped inside npm scripts. Here is how the package.json looks:

"config": {
    "binaries": "./dist/kiosk-demo-electron-win32-x64"
"scripts": {
    "clean": "rimraf ./dist",
    "pack": "cross-env DEBUG=electron-packager electron-packager ./src --platform=win32 --arch=x64 --asar --overwrite --out ./dist",
    "heat": "wixtoolset-compiler heat --args=\"dir %npm_package_config_binaries% -v -ag -cg ElectronBinaries -var var.SourceDir -dr APPLICATIONROOTDIRECTORY -srd -suid -sfrag -sreg -out ./dist/heat.wxs\"",
    "copyScripts": "cross-conf-env copyfiles -f ./tools/scripts/*.* npm_package_config_binaries",
    "candle": "wixtoolset-compiler candle --args=\"-v -ext WixUtilExtension -dSourceDir=%npm_package_config_binaries% ./tools/product.wxs ./dist/heat.wxs -out ./dist/\"",
    "light": "wixtoolset-compiler light --args=\"-v -ext WixUtilExtension ./dist/product.wixobj ./dist/heat.wixobj -out ./dist/KioskDemoElectron.msi\"",
    "build": "run-s heat copyScripts candle light",
    "dist": "run-s clean pack build"
"devDependencies": {
    "copyfiles": "^1.2.0",
    "cross-conf-env": "^1.1.2",
    "cross-env": "^5.0.5",
    "electron": "^1.7.8",
    "electron-packager": "^9.1.0",
    "npm-run-all": "^4.1.1",
    "rimraf": "^2.6.2",
    "wixtoolset-compiler": "^1.0.3"

Next, generate the installer in the /dist folder:

npm run dist

The installer can simply be invoked by:


Image Featured

Image intune 1024 215 511Distributing the generated .msi with Intune in the Azure Portal

Continuous Integration


We wanted to make sure we had a way to verify that changes in Electron app or PowerShell script continue to produce an installer that correctly sets the app in kiosk mode, so we used AppVeyor CI to build and execute the installer on AppVeyor’s Windows build agent as well as produce an installation log file.

Here is the appveyor.yml:

image: Visual Studio 2017
  - ps: Install-Product node 9
  - npm install npm@latest -g
  - npm install
 - npm run dist
 - cd dist
 - msiexec /i "KioskDemoElectron.msi" /l*v "install.log" KIOSK_USERNAME=%username%
  - path: .\dist\*.msi
  - path: .\dist\*.log

Travis CI

Building Windows installers on a Linux environment is uncommon, and we ran into a few challenges.

In order to isolate Wine and Winetricks setup, we used an existing Linux Docker image which contained WiX Toolset, Wine and Node.js, to create our app’s Docker image.

FROM syedhassaanahmed/wix-node
RUN mkdir /home/wix/src
WORKDIR /home/wix/src
COPY . .
RUN npm install
RUN npm run dist:wine

We then added these Docker commands to npm scripts which build the image, create the container and copy the artifacts from the container to host machine.

    "docker:build": "cross-conf-env docker build -t npm_package_name .",
    "docker:create": "cross-conf-env docker create --name npm_package_name npm_package_name",
    "docker:copy": "cross-conf-env docker cp npm_package_name:/home/wix/src/dist/ ./dist",
    "docker:remove": "cross-conf-env docker rm -f npm_package_name",
    "dist:docker": "run-s docker:build docker:create clean docker:copy docker:remove"

Here is the final Travis CI configuration:

language: node_js
  - "9"
  - docker
  - npm run dist:docker
  email: false


Through our collaboration with the customer, we were able to build a simple Electron-based app that we could deploy to a broad range of devices using Microsoft Intune.  While we used a simple Electron app here, you can easily use the scripts and code that we developed here for other projects where you want to manage and customize the deployment of Electron apps.


You can reach out to us with feedback and questions in the comments below.