Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

drawio-desktop Administrator & Developer Guide

Generated April 2026 from jgraph/drawio-desktop at 5986014

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the operational and architectural reference for drawio-desktop, the Electron-based desktop edition of the upstream jgraph/drawio web diagram editor. It targets four audiences: IT administrators planning enterprise deployments (Group Policy, Intune, Microsoft Store, snap, flatpak), security reviewers auditing the application's threat model, developers contributing to the desktop wrapper, and architects integrating drawio-desktop with downstream automation (CI export pipelines, file-server hosting, document workflows).

The User Guide covers everyday end-user operation. This guide covers the parts a non-developer end user does not need to think about: the Electron main-process architecture, the full configuration surface, the build pipeline (electron-builder, fuses, notarization, snap), the IPC channel inventory, the sandboxing posture, and how the desktop wrapper relates to the upstream submodule.


1. Architecture Overview

drawio-desktop is intentionally a thin shell. It is not a re-implementation of the diagram editor; it is an Electron host that loads the upstream jgraph/drawio web bundle from disk, exposes a strictly bounded file-system bridge over IPC, and adds three things the web edition cannot do: native OS file associations, a headless command-line export pipeline, and an in-app auto-updater.

1.1 Component diagram

+------------------------------------------------------------------+
| Operating System                                                 |
|                                                                  |
|  +----- Electron Main Process (Node 20) ------------------+      |
|  |                                                        |      |
|  |  src/main/electron.js (main entry)                     |      |
|  |    |- BrowserWindow #1  --[loads file://...index.html]-+----+ |
|  |    |- BrowserWindow #2  --[loads file://...index.html]-+--+ | |
|  |    |- export-only hidden window (CLI mode)             |  | | |
|  |    |- electron-updater  --[GitHub Releases JSON]-------+--+-+-+--> github.com
|  |    |- ipcMain (rendererReq dispatcher)                 |  | | |
|  |    |- session.webRequest (CSP + file:// gate)          |  | | |
|  |    |                                                   |  | | |
|  |    fs / path / shell / dialog / clipboard / store      |  | | |
|  +----+-----------------------------------------------+---+  | | |
|       |                                               |      | | |
|       | preload.js (legacy log)   electron-preload.js |      | | |
|       | contextBridge.exposeInMainWorld('electron',{})|      | | |
|       v                                               v      | | |
|  +-----------------------------------------------------+     | | |
|  | Renderer Process (Chromium 130 inside Electron 40)  |  ---+ | |
|  |   bundled drawio/src/main/webapp/index.html         |       | |
|  |   - mxGraph editor                                  |       | |
|  |   - shapes, libraries, plugins                      |       | |
|  |   - save/load via window.electron.request(...)      |       | |
|  +-----------------------------------------------------+       | |
|                                                                | |
|  Local filesystem:                                             | |
|    *.drawio / *.vsdx           <-- user diagrams               | |
|    %APPDATA%/draw.io/          <-- electron-store config       | |
|    plugins/                    <-- per-user plugin JS          | |
|    .$<file>.dtmp               <-- in-flight drafts            | |
|    .$<file>.bkp                <-- backup of last save         | |
+----------------------------------------------------------------+--+

Note

The diagram is the operational reality, not a design intent. The editor itself runs in the renderer over file://, the wrapper enforces a CSP that pins default-src 'self' to that origin, and every privileged action (file save, plugin install, dialog open) crosses an IPC boundary that validates the sender frame against codeUrl. Compromise of the renderer alone does not yield arbitrary code execution outside the bundled drawio/src/main/webapp directory.

1.2 Component inventory

Component Path Role
Main entry src/main/electron.js Electron main process: window lifecycle, CLI export, IPC dispatcher, auto-updater, file I/O
Renderer preload src/main/electron-preload.js Exposes window.electron and window.process to the renderer via contextBridge
Legacy preload preload.js Minimal pre-load logger (registered as a webPreferences.preload candidate)
Update fuse src/main/disableUpdate.js Build-time switch to permanently disable the updater (returns false upstream; package channels can patch to return true)
Upstream editor drawio/ (git submodule) The jgraph/drawio web app — bundles into drawio/src/main/webapp/ and is loaded by BrowserWindow.loadURL
Submodule sync sync.cjs Helper script invoked via npm run sync to refresh the upstream submodule and copy webapp assets
Build configs electron-builder-*.json Six platform-specific electron-builder configurations (see Section 3.4)
Package manifest package.json npm name draw.io, version field, scripts, dependencies, engines.node >=20

1.3 Technology stack

Layer Technology Version
Runtime Electron ^40.8.4
Node.js Bundled by Electron >=20 (pinned in engines)
Renderer Chromium (bundled with Electron 40) matched to Electron release
Window persistence electron-store ^11.0.2
Logging electron-log ^5.4.3
Progress UI electron-progressbar ^2.2.1
Context menu electron-context-menu ^4.1.1
Auto-updater electron-updater ^6.8.3
CLI parser commander ^14.0.3
PDF combine @cantoo/pdf-lib ^2.6.1
HTTP/compression compression, buffer, crc mature deps
Native binary signing @electron/notarize (mac), @electron/fuses ^3.1.1, ^1.8.0
Build packager electron-builder ^26.8.1

1.4 Process model

drawio-desktop runs one main process and N renderer processes (one per editor window plus one hidden window during CLI export). The single-instance lock guarantees that a second drawio invocation forwards its arguments to the first process via app.requestSingleInstanceLock() / second-instance rather than starting a new main process.

Renderer-to-main communication is asynchronous and goes through three IPC paths:

  1. ipcMain.on('rendererReq', ...) — the umbrella dispatcher used for all file-system operations and most UI-bridging actions. Renderer calls window.electron.request({action: 'saveFile', ...}, callback, error) and the main process routes by args.action.
  2. Direct ipcMain.on(<event>, ...) handlers — high-frequency or stateful events such as export, newfile, toggleSpellCheck, zoomIn, checkForUpdates, app-load-finished.
  3. Main-to-renderer pushes — webContents.send('args-obj', ...) (deliver CLI args to the just-loaded renderer), 'fileChanged' (file watcher), 'maximize' / 'unmaximize' / 'resize' (window state).

Note

Every IPC handler that mutates state or returns sensitive data calls validateSender(e.senderFrame) first. The validator rewrites the sender's URL to uppercase the Windows drive letter, then checks startsWith(codeUrl) where codeUrl is the on-disk file:// URL of the bundled drawio/src/main/webapp/. Any cross-origin frame (an injected iframe, a navigated-away BrowserWindow) fails the check and the handler returns null without performing the action.


2. System Requirements

2.1 Runtime requirements

Resource Floor Notes
OS Windows 10 (1809), macOS 11 Big Sur, Linux glibc 2.31 Matches Electron 40's published platform matrix
CPU x86_64, ARM64 (Win/Mac/Linux), x86 (ia32) Windows Snap built only for x64 (base: core20)
RAM 1 GB free 4 GB+ for diagrams with 1000+ shapes
Disk 300 MB install Plus userData growth (drafts, plugins, config, Chromium cache)
GPU Optional (set --disable-acceleration if compositing artifacts)
Network Outbound HTTPS to github.com for updates only Suppressible via DRAWIO_DISABLE_UPDATE=true

2.2 Build host requirements

Resource Requirement
Node >=20 (per package.json engines.node), recommended via nvm use against .nvmrc
Git 2.x with submodule support (the drawio/ directory is a submodule)
Disk 5 GB+ for node_modules plus electron-builder artifact dist
OS for cross-compile Native — Windows builds need Windows host, macOS DMG/notarization needs macOS host with valid Apple Developer ID
Code signing certificates Authenticode for Windows (NSIS/MSI), Apple Developer ID + notarization credentials for macOS
GH_TOKEN env Required for --publish always invocations of electron-builder

2.3 Network policy for managed deployments

Endpoint Purpose Suppressible?
https://github.com/jgraph/drawio-desktop/releases/... Auto-updater fetches latest.yml and signed installer Yes via DRAWIO_DISABLE_UPDATE=true
https://convert.diagrams.net/node/export Default value of the URL export query parameter (used only by the upstream web app's export-server feature, which the desktop edition routes locally) Override via urlParams.json
https://fonts.googleapis.com and https://fonts.gstatic.com Google Fonts integration Off by default; gated behind isGoogleFontsEnabled electron-store key
https://www.drawio.com Reached only via the macOS About menu's shell.openExternal(...) click User-triggered

Important

A fully air-gapped deployment is supported — set DRAWIO_DISABLE_UPDATE=true system-wide, leave isGoogleFontsEnabled at false (the default), and ensure no urlParams.json exists in the working directory. The CSP enforced by the main process pins connect-src 'self', so the renderer cannot reach external endpoints unless explicitly relaxed.


3. Configuration Reference

drawio-desktop has four configuration surfaces. They are listed here in the order they take effect.

3.1 Build-time fuse

src/main/disableUpdate.js:

export function disableUpdate() { return false; }

This single function is the kill-switch for the updater. The official build always returns false (updater enabled). Distribution channels that handle updates externally (snap, flatpak, AppX, third-party redistributors) patch this file to return true before packaging. The result is OR'd with DRAWIO_DISABLE_UPDATE, the --disable-update CLI flag, and the /.flatpak-info filesystem check.

3.2 Environment variables

Variable Type Default Description Required?
DRAWIO_DISABLE_UPDATE 'true' / unset unset Disables the in-app auto-updater No
DRAWIO_SILENT_UPDATE 'true' / unset unset Auto-download and install on quit when updates are not disabled No
DRAWIO_ENV 'dev' / unset unset Enables developer mode (DevTools auto-open, log lines, dev URL params) No
ELECTRON_USER_DATA path platform default Override Electron's userData directory; affects config, plugins, drafts, cache No
GH_TOKEN string — Used by electron-builder during release; never read at runtime Required only for release-* scripts
XDG_CONFIG_HOME path ~/.config Linux: relocate the draw.io/ config directory No

3.3 Command-line flags (Electron main process)

These five flags are interpreted by the desktop wrapper itself before any export-mode parsing:

Flag Source line Effect
--disable-update electron.js:117 Same as DRAWIO_DISABLE_UPDATE=true
--silent-update electron.js:122 Same as DRAWIO_SILENT_UPDATE=true
--disable-acceleration electron.js:132 app.disableHardwareAcceleration() before any window opens
--enable-plugins passed via commander, set on enablePlugins global Allows the plugins/ directory's JS to load via file://
--no-sandbox filtered out of program.args Standard chromium switch; preserved on the parent argv but stripped from input file paths

3.4 electron-builder configurations

Six dedicated builder configs exist. Each shares the common appId: "com.jgraph.drawio.desktop", asar: true, directories.output: "./dist/", and the standard file-association block for .drawio (mime application/vnd.jgraph.mxfile, role Editor) and .vsdx (mime application/vnd.visio, role Editor).

File Targets Architectures Publishing Special
electron-builder-win.json NSIS (drawio-<v>-windows-installer.exe), MSI (drawio-<v>.msi) x64 github Both targets perMachine, runAfterFinish: false, createDesktopShortcut: false
electron-builder-win-arm64.json NSIS, portable EXE arm64 github Same NSIS settings, portable artifact name drawio-arm64-<v>-windows-arm64-no-installer.exe
electron-builder-win32.json NSIS only ia32 github Legacy 32-bit
electron-builder-appx.json AppX x64 github Publisher CN=9E628CCB-BE04-4557-A5A8-81EC34B09733, identity draw.io.draw.ioDiagrams, display name "draw.io Diagrams"
electron-builder-linux-mac.json macOS: zip + dmg (x64, arm64, universal); Linux: AppImage, deb, rpm (each x64+arm64) many github Mac hardenedRuntime: true, gatekeeperAssess: false, entitlements build/entitlements.mac.plist, afterSign: build/notarize.mjs, RPM with _build_id_links none and --rpm-digest=sha256
electron-builder-snap.json snap x64 (no publish) base: core20, plugs default + removable-media

Every config invokes afterPack: build/fuses.cjs — this runs @electron/fuses to lock down the resulting binary's runtime by flipping Electron security fuses (e.g. disable Node CLI args, enforce ASAR integrity, etc.). The exact fuse list is defined in the project's build/fuses.cjs script.

Important

The files glob is ["**/*", "!**/WEB-INF{,/**}"] for every config. The WEB-INF exclusion matters because the upstream drawio/src/main/webapp/ was originally a Java servlet bundle and still contains WEB-INF/ directories that have no role in Electron and would unnecessarily bloat the asar archive.

3.5 npm scripts (package.json)

Script Command Use case
start electron . Local development run
sync node ./sync.cjs Pull latest upstream drawio submodule and stage assets
release-win electron-builder --config electron-builder-win.json --publish always x64 NSIS + MSI release
release-win32 electron-builder --config electron-builder-win32.json --publish always ia32 NSIS release
release-win-arm64 electron-builder --config electron-builder-win-arm64.json --publish always arm64 NSIS + portable release
release-appx electron-builder --config electron-builder-appx.json --publish always Microsoft Store AppX release
release-linux electron-builder --config electron-builder-linux-mac.json --publish always Linux + macOS release
release-snap electron-builder --config electron-builder-snap.json --publish never Snap (publishes manually to Snapcraft)

Caution

All release-* except release-snap use --publish always. Running them with no GH_TOKEN fails the publish step but the artifacts still land in ./dist/. Running them on a fork with a valid GH_TOKEN will publish to that fork's GitHub Releases — never run them on a PR branch with an upstream token configured.

3.6 electron-store keys

Persistent JSON stored at app.getPath('userData')/config.json via the electron-store package. Defaults are the values used when the key is absent; the wrapper writes a value back on first relevant interaction.

Key Type Default Set by
lastWinSize string "w,h,x,y,max,fs" "1200,800,0,0,false,false" Every window close (rememberWinSize())
enableSpellCheck boolean true on macOS (isMac), false elsewhere toggleSpellCheck IPC
enableStoreBkp boolean true toggleStoreBkp IPC
isGoogleFontsEnabled boolean false toggleGoogleFonts IPC
dontCheckUpdates boolean false Set to false after manual update check

3.7 urlParams.json overlay

Loaded from process.cwd() + '/urlParams.json' if present. Merges into the URL query passed to BrowserWindow.loadURL. Default query (from electron.js:182):

{
  dev: 0, test: 0,
  gapi: 0, db: 0, od: 0, gh: 0, gl: 0, tr: 0,
  browser: 0, picker: 0,
  mode: 'device',
  export: 'https://convert.diagrams.net/node/export',
  disableUpdate: 0|1,
  enableSpellCheck: 0|1,
  enableStoreBkp: 0|1,
  isGoogleFontsEnabled: 0|1
}

mode: 'device' forces the upstream web app to use local device storage; the gapi/od/gh/gl/tr flags disable Google Drive, OneDrive, GitHub, GitLab, and Trello cloud backends respectively.


4. Deployment & Scaling

drawio-desktop is a per-user installable native application. There is no server, no database, no horizontal scaling axis — deployment is the act of installing the binary on each endpoint. The scaling concern is therefore strictly endpoint management.

4.1 Channel selection

Audience Recommended channel Update path
Individual user, latest features NSIS exe / DMG / AppImage from GitHub Releases In-app updater
Enterprise Windows fleet MSI via Group Policy or Intune Disabled in-app updater; managed via SCCM/Intune redeployment
Microsoft Store users AppX Microsoft Store auto-updates
Ubuntu/snap users snap snap refresh
Flatpak users (community build) flatpak flatpak update (in-app updater auto-disabled)
Headless CI export AppImage in container Pinned version, no updates

4.2 Group Policy / Intune (Windows)

The MSI variant exists specifically for Group Policy software installation and Intune Win32 app deployment.

# Install (silent, no restart):
msiexec /i drawio-29.6.6.msi /qn /norestart

# Uninstall:
msiexec /x drawio-29.6.6.msi /qn /norestart

For Intune Win32 app, package the MSI with the standard IntuneWinAppUtil.exe and configure detection via the MSI product code.

To suppress the in-app updater fleet-wide, push a system environment variable DRAWIO_DISABLE_UPDATE=true via Group Policy Preferences → Environment.

4.3 macOS managed deployment (MDM)

DMG → drag-install is the user-facing pattern; for MDM (Jamf, Kandji, Mosyle, Workspace ONE) repackage as a .pkg:

pkgbuild --component "draw.io.app" \
         --identifier com.jgraph.drawio.desktop \
         --version 29.6.6 \
         --install-location /Applications \
         drawio-29.6.6.pkg

Set the launch-environment variable via a LaunchAgent plist or your MDM's environment-variable mechanism.

4.4 Linux fleet

For Debian-derived fleets, host the .deb on an apt repository (e.g. apt-mirror, aptly); for RHEL/Fedora, an rpm repository. AppImage suits portable per-user installs without root.

For snap-managed fleets, set the track to stable and use snap channels for staged rollouts. Be aware of issue #2219 for very recent Ubuntu releases.

4.5 Container / CI

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
    libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xdg-utils \
    libatspi2.0-0 libuuid1 libsecret-1-0 xvfb fuse && \
    apt-get clean
COPY drawio-amd64-29.6.6.AppImage /usr/local/bin/drawio
RUN chmod +x /usr/local/bin/drawio
ENV DRAWIO_DISABLE_UPDATE=true
ENTRYPOINT ["xvfb-run", "drawio", "--no-sandbox", "--export"]

Use this image to bulk-convert .drawio files inside CI:

docker run --rm -v $(pwd):/work drawio-export \
  --format png --recursive --output /work/out /work/diagrams

Tip

AppImage requires FUSE inside the container. If the runner refuses FUSE, extract the AppImage offline (./drawio.AppImage --appimage-extract) and run the extracted squashfs-root/AppRun directly instead.

4.6 Scaling considerations

drawio-desktop processes are not designed for parallelism within a single binary — each window runs an independent renderer. For headless CI batch export, parallelize by launching N container instances in parallel, each with a slice of the input file list. The CLI export path holds a hidden window, processes one file at a time, and exits — there is no internal worker pool to tune.


5. Operations

5.1 Health and version visibility

drawio --version
29.6.6

The version is read from app.getVersion(), which sources package.json:version packed into the asar archive. There is no /health endpoint — drawio-desktop does not run a server.

5.2 Log files and diagnostics

OS Log location
Windows %USERPROFILE%\AppData\Roaming\draw.io\logs\main.log
macOS ~/Library/Logs/draw.io/main.log
Linux ~/.config/draw.io/logs/main.log

The default file-level threshold is error; raise it with electron-log configuration in a development build, or run with --enable-logging to send Chromium logs to the controlling terminal.

autoUpdater.logger.transports.file.level = 'error' is set explicitly in electron.js:124-125, so update-related events of info/warn severity are not written to disk by default.

5.3 Routine maintenance

  • [x] Verify latest.yml on the GitHub Releases page after each tag push (automatic via --publish always)
  • [ ] Quarterly: audit node_modules for vulnerable transitive deps (npm audit)
  • [ ] When upstream jgraph/drawio cuts a new major: npm run sync, smoke test
  • [ ] Before each release: confirm all six electron-builder-*.json configs build clean
  • [ ] After each release: validate notarization status of the macOS DMG via spctl -a -vvv "draw.io.app"
  • [ ] Every 6 months: review the CSP in electron.js:480-487 against current upstream needs

5.4 Data on disk

Path Format Owner Backup
<userData>/config.json JSON via electron-store wrapper Optional — small, easy to recreate
<userData>/plugins/*.js JS user-installed Yes — irrecoverable
<dir>/.$<filename>.dtmp XML draft wrapper No — ephemeral
<dir>/.$<filename>.bkp XML wrapper Recovery copy of last save
User's .drawio files Compressed XML or raw mxfile user Yes — diagram source

Note

The wrapper's draft system uses prefix .$ for both drafts (.dtmp) and backups (.bkp). Older versions used ~$ — getFileDrafts() automatically migrates old-prefixed files to the new prefix on read.

5.5 Common operations runbooks

Reset a user's drawio-desktop to defaults

  1. Quit the application (verify no draw.io.exe / draw.io process is running).
  2. Back up <userData>/config.json to a safe location.
  3. Delete <userData>/config.json.
  4. Optionally clear <userData>/Cache/ and <userData>/Code Cache/ (Chromium caches).
  5. Relaunch — defaults are regenerated.

Disable updates fleet-wide

Push the environment variable via your management tool:

  • Windows: GPP Environment Variable, system scope: DRAWIO_DISABLE_UPDATE=true
  • macOS: launchctl setenv DRAWIO_DISABLE_UPDATE true (per session; for persistent use a LaunchDaemon)
  • Linux: /etc/environment or /etc/profile.d/drawio.sh

Tip

On managed Windows endpoints, you can additionally remove the Check for updates menu item from the macOS bundle by pre-setting disableUpdate — the wrapper splices it out of the menu template at runtime when disableUpdate is true. This avoids confusing users with a non-functional menu item.

Roll back to a previous version

drawio-desktop releases are immutable; download the desired older .exe/.dmg/.deb/.rpm from the Releases archive and reinstall. The auto-updater will offer to update again on next launch unless suppressed.


6. Performance

6.1 Startup performance

Phase Duration (typical) Notes
Electron main process boot 100-300 ms Hardware dependent
electron-store initialization <50 ms JSON file I/O; can fail silently → store = null and defaults take over
First BrowserWindow creation + loadURL 200-500 ms Loads index.html from local file://
Upstream draw.io webapp boot 1-3 seconds mxGraph editor initialization, library catalogue load
Auto-updater check (when enabled) Async, off main thread Single HTTPS GET to GitHub Releases JSON

Disable hardware acceleration with --disable-acceleration if compositor or GPU driver issues add seconds to launch.

6.2 Memory footprint

Component Idle RAM Notes
Main process ~80-150 MB Node + Electron baseline
Renderer (per window) ~150-300 MB Chromium baseline + editor JS heap
GPU process ~50-80 MB Bundled Chromium GPU

Five open windows therefore reserve ~1-1.5 GB. Each editor window is independent — the wrapper does not pool renderers.

6.3 Export performance

The CLI export pipeline serializes file processing — the hidden BrowserWindow loads each file, renders, writes, and proceeds. Per-file overhead:

Operation Cost
Single-page PNG/SVG render 200-800 ms after first warmup
First file (warmup includes editor boot) +1-2 seconds
Multi-page PDF combine (mergePdfs) +50 ms per page; uses @cantoo/pdf-lib
VSDX import (per file) +1-3 seconds (parses ZIP into XML before render)

For hundreds of files, parallelize by spawning multiple containers / processes; do not attempt to parallelize within a single drawio-desktop process.

6.4 Render quality vs. performance

--scale and --width directly drive output resolution and therefore CPU and memory. Aggressive scaling (e.g. --scale 4) on dense diagrams can OOM the renderer; prefer --width <px> to clamp the worst case.

Tip

When exporting many large diagrams to PDF, pass --all-pages rather than per-page invocations — the wrapper combines pages in-memory via mergePdfs() once, avoiding the editor warmup penalty for each page.


7. Security

drawio-desktop's security model is defense-in-depth around a single principle: the renderer is treated as untrusted even though it loads bundled code, because the diagram XML it parses is user-supplied and may contain pasted content from arbitrary sources.

7.1 Electron security posture

Setting Value Why
contextIsolation true Renderer sees only the explicitly bridged window.electron object — no direct Node access
webviewTag false <webview> cannot be created — prevents arbitrary-origin nested rendering
webSecurity true Same-origin policy enforced for the renderer
disableBlinkFeatures 'Auxclick' Disables middle-click navigation that could bypass will-navigate handler
nodeIntegration (renderer) not enabled require() is unavailable in the renderer
sandbox not enabled Disabled because it broke snap packaging (see comment on electron.js:219)
Renderer preload electron-preload.js Uses contextBridge.exposeInMainWorld only — no prototype pollution

Warning

The Chromium sandbox is not enabled. The comment at electron.js:219 documents the reason: enabling app.enableSandbox() broke snap packaging. The wrapper compensates with strict sender validation, CSP, and file-protocol gating, but a sandbox-enabled build would still be a stronger posture. Track this as a known trade-off when assessing risk.

7.2 Content Security Policy

Every renderer response is rewritten with a strict CSP via session.webRequest.onHeadersReceived:

default-src 'self';
script-src 'self';
connect-src 'self'[ + https://fonts.googleapis.com https://fonts.gstatic.com when isGoogleFontsEnabled];
img-src * data:;
media-src *;
font-src * data:[ + https://fonts.googleapis.com when isGoogleFontsEnabled];
frame-src 'self';
style-src 'self' 'unsafe-inline'[ + https://fonts.googleapis.com when isGoogleFontsEnabled];
base-uri 'none';
child-src 'self';
object-src 'none';

The CSP applies to every URL except config-editor.html, which is exempted by details.url.indexOf('config-editor.html') >= 0 to permit its own inline configuration scripts.

7.3 File protocol gating

session.defaultSession.webRequest.onBeforeRequest({urls: ['file://*']}, (details, callback) => {
  if (!url.startsWith(codeUrl) && (!isPluginsEnabled() || !url.startsWith(pluginsCodeUrl))) {
    callback({cancel: true});
  }
});

Every file:// request is intercepted: requests that do not start with the bundled webapp directory (codeUrl) or — when plugins are enabled — the per-user plugins directory are cancelled. This blocks an attacker from inducing the renderer to load arbitrary files from the user's disk via crafted diagram XML (e.g. <image href="file:///etc/passwd"/>).

7.4 External URL whitelist

const allowedUrls = /^(?:https?|mailto|tel|callto):/i;

function openExternal(url) {
  if (allowedUrls.test(url)) {
    shell.openExternal(url);
    return true;
  }
  return false;
}

Only http, https, mailto, tel, and callto schemes are passed to shell.openExternal. Custom schemes (javascript:, data:, file:, vbscript:, anything else) are silently rejected. This protects against URL-handler-based RCE chains in the host OS.

7.5 Navigation lockdown

In app.on('web-contents-created'):

contents.on('will-navigate', (event, navigationUrl) => { event.preventDefault() });
contents.setWindowOpenHandler(({ url }) => {
  if (url.startsWith('about:blank')) { /* allow with isolated webPreferences */ }
  else if (!openExternal(url)) { return {action: 'deny'}; }
});
contents.on('will-attach-webview', (event, ...) => { event.preventDefault() });
  • will-navigate is preempted — the renderer cannot navigate the page to a different URL.
  • window.open is constrained to either about:blank (with contextIsolation: true inherited) or external URLs through openExternal's whitelist.
  • Webview attachment is blocked at the event level (defense in depth alongside webviewTag: false).

7.6 IPC sender validation

Every privileged IPC handler calls validateSender(e.senderFrame) before acting:

function validateSender(frame) {
  return frame.url.replace(/\/.\:\//, str => str.toUpperCase()).startsWith(codeUrl);
}

The drive-letter normalization handles Windows file:///c:/ vs file:///C:/ discrepancies. A sender whose URL does not start with the bundled webapp's file:// prefix gets null returned and the action is silently dropped.

7.7 Save-path containment

Every saveFile / writeFile / saveDraft rejects paths that resolve under the application install directory (appBaseDir):

if (!checkFileContent(data) || path.resolve(fileObject.path).startsWith(appBaseDir)) {
  throw new Error('Invalid file data');
}

This prevents an attacker who has compromised the renderer (e.g. via a malicious plugin) from overwriting bundled application files. Combined with the asar archive's read-only nature, this is belt-and-braces.

7.8 Auto-update integrity

electron-updater validates the downloaded installer against the SHA-512 published in latest.yml before applying. The first install must be trusted manually (the .exe / .dmg / .deb is signed but the user is responsible for verifying the GitHub release tag). Subsequent updates inherit trust from the installed binary's signature chain.

Caution

Update artifacts are not signed with a per-release manifest signature beyond the platform-native code signature on the binary itself. A compromise of github.com/jgraph/drawio-desktop repo write access could publish a malicious latest.yml pointing at a malicious binary. For high-assurance fleets, disable electron-updater (DRAWIO_DISABLE_UPDATE=true) and manage updates through your own signed-package channel (Microsoft Endpoint Manager, Munki, etc.).

7.9 Plugin trust model

Plugins are not sandboxed beyond the file-protocol whitelist. Once loaded (--enable-plugins plus installPlugin IPC), a plugin runs in the renderer with the same window.electron bridge access as the core app. Treat plugin installation as code installation. Vet plugin source before allowing in managed environments.

7.10 Notarization and code signing

Platform Mechanism Builder hook
macOS Apple Developer ID + notarization afterSign: build/notarize.mjs (uses @electron/notarize)
Windows NSIS/MSI/AppX Authenticode Configured via electron-builder env vars (CSC_LINK, CSC_KEY_PASSWORD)
Linux None (distros sign packages independently if at all) —
All Electron fuses (runtime hardening) afterPack: build/fuses.cjs (uses @electron/fuses)

8. Developer Internals

8.1 Source tree

Path Purpose
src/main/electron.js Main process: window lifecycle, CLI export, IPC dispatch, auto-updater
src/main/electron-preload.js Renderer preload — contextBridge.exposeInMainWorld('electron', {...})
src/main/disableUpdate.js Build-time fuse for the auto-updater
preload.js Legacy minimal preload (logs to console, exposes nothing)
sync.cjs Submodule + assets sync utility
package.json npm manifest — name draw.io, scripts, deps
package-lock.json npm lockfile
.gitmodules Declares drawio/ submodule
.nvmrc Pinned Node version for nvm use
electron-builder-*.json Six platform-specific builder configs (see Section 3.4)
LICENSE Apache-2.0
README.md High-level project description, end-user pointer
DEVELOPMENT.md Build instructions for contributors
CODE_OF_CONDUCT.md, SECURITY.md Standard project policies
dependency-diagram.png, screenshot.png Static assets
doc/RELEASE_PROCESS.md Maintainer release runbook
drawio/ Git submodule → jgraph/drawio (the actual editor)

8.2 Main process flow (src/main/electron.js)

The main module is one large file (~2900 lines) that boots in this order:

  1. Imports (lines 86-103) — Electron, fs, path, third-party modules.
  2. Store init (105-115) — instantiates electron-store inside try/catch; on failure, store = null and the wrapper falls back to defaults silently.
  3. Update kill-switch resolution (117-122) — combines disableUpdate.js return value, env var, CLI flag, and /.flatpak-info filesystem probe.
  4. Hardware acceleration switch (132-135) — --disable-acceleration calls app.disableHardwareAcceleration() immediately.
  5. Context-menu setup (138-157) — electron-context-menu configured for spell check + paste-and-match-style.
  6. Default URL query construction (182-216) — composes the parameters passed to BrowserWindow.loadURL, optionally overlaid with urlParams.json from cwd.
  7. createWindow() (244-459) — used for every visible editor window. Handles size restoration, position bounds checking, the dirty-document close prompt, the Ctrl+Shift+V interception.
  8. app.whenReady() block (468-1375) — registers the CSP, registers the file:// gate, parses commander flags, dispatches export mode (700-1064) or interactive mode (1071-1374).
  9. App lifecycle hooks (1377-1491) — before-quit, window-all-closed, activate, will-finish-launching (the macOS open-file handler), web-contents-created (navigation lockdown).
  10. Auto-updater handlers (1493+) — error, update-available, download-progress, update-downloaded events, progress bar UI.
  11. Helper functions for PNG metadata I/O, PDF combine, file content validation, draft management, plugin install, dialogs, font enumeration.
  12. The umbrella ipcMain.on('rendererReq', ...) dispatcher (3232-3331) — switch on args.action.

8.3 IPC channel inventory

Direct channels (registered with ipcMain.on(...) outside the umbrella dispatcher):

Channel Direction Purpose
openDevTools renderer → main Open DevTools on the focused window
isModified-result renderer → main Reply to the isModified query during window close
newfile renderer → main Spawn a new editor window
toggleSpellCheck renderer → main Flip and persist enableSpellCheck
toggleStoreBkp renderer → main Flip and persist enableStoreBkp
toggleGoogleFonts renderer → main Flip and persist isGoogleFontsEnabled
toggleFullscreen renderer → main Toggle fullscreen on the focused window
checkForUpdates renderer → main Trigger autoUpdater.checkForUpdates()
zoomIn / zoomOut / resetZoom renderer → main Adjust webContents.zoomFactor
app-load-finished renderer → main Renderer signals it has fully booted
export renderer → main Trigger an export (interactive UI export path)
import-success / import-error renderer → main VSDX import callbacks
xml-data / xml-data-error / svg-data renderer → main Export-pipeline payload deliveries
render-finished / export-finalize renderer → main Render-pipeline completion
args-obj main → renderer Deliver CLI args after did-finish-load
removeDraft / draftRemoved main ↔ renderer Coordinate draft cleanup on close
fileChanged main → renderer File-watcher notification
maximize / unmaximize / resize main → renderer Window state notifications
mainResp main → renderer Reply payload for any rendererReq

Umbrella rendererReq actions (every privileged operation goes through here):

Action Effect
saveFile Atomic save with verify-on-readback, retry up to 3
writeFile Lower-level write
saveDraft Write .dtmp draft, hidden on Windows via attrib +h
getFileDrafts Enumerate drafts beside a file (auto-migrating ~$ to .$)
getDocumentsFolder OS Documents path
checkFileExists fs.existsSync for path parts
showOpenDialog / showSaveDialog Native dialog wrappers (sets dialogOpen flag)
installPlugin / uninstallPlugin / getPluginFile Plugin lifecycle
isPluginsEnabled Query the runtime flag
dirname path.dirname proxy
readFile Read file with encoding
clipboardAction Clipboard I/O proxy
deleteFile fs.unlink
fileStat fs.stat
isFileWritable fs.access(W_OK)
windowAction Maximize/unmaximize/close/isMaximized/removeAllListeners
openExternal URL whitelist + shell.openExternal
watchFile / unwatchFile fs.watchFile proxy with fileChanged push
exit app.quit()
getLocalFonts OS-specific font enumeration (PowerShell on Windows, fc-list elsewhere)
isFullscreen Query focused window

8.4 Build pipeline

  1. npm install — installs runtime + dev deps; builds native modules if any.
  2. npm run sync — node ./sync.cjs updates the upstream drawio submodule and copies src/main/webapp/ into the location packaged into the asar.
  3. electron-builder --config <config> — packs source into asar (asar: true), runs afterPack: build/fuses.cjs to harden the binary, on macOS runs afterSign: build/notarize.mjs, on Linux/Windows produces native installers (NSIS/MSI/AppX/DEB/RPM/AppImage/snap).
  4. Publish — with --publish always, installers + latest.yml upload to GitHub Releases via the GitHub provider, using GH_TOKEN.

The afterPack fuses script invokes @electron/fuses to flip the runtime fuses Electron exposes, locking down the produced binary's accepted command-line switches (preventing e.g. --inspect-brk injection).

8.5 Code patterns

  • Sender validation everywhere. Every IPC handler that does I/O calls validateSender(e.senderFrame). Pattern: if (e != null && !validateSender(e.senderFrame)) return null;. Adding a new IPC handler must include this guard.
  • Null-safe electron-store. store may be null if init failed; every read uses store != null ? store.get(...) : default. Every write is guarded if (store != null) store.set(...).
  • Win32 path normalization. frame.url.replace(/\/.\:\//, str => str.toUpperCase()) is used in two places to canonicalize Windows drive letters before string comparison.
  • O_SYNC writes for diagrams. Save paths use fs.constants.O_SYNC | O_CREAT | O_WRONLY | O_TRUNC; backups follow the same pattern. Reads after write verify byte-for-byte.
  • Atomic file rename for drafts. Old ~$-prefixed drafts are migrated to .$ via fsProm.rename rather than read-then-write.

8.6 Testing strategy

There is no test suite checked into the repository (no test/, no *.test.js, no package.json test script). Quality gates are: end-to-end manual testing per platform during the release process (see doc/RELEASE_PROCESS.md), upstream jgraph/drawio testing of the editor itself, and community feedback via GitHub Issues.

Tip

If you contribute a desktop-wrapper change, attach a manual test plan to your PR: a list of the IPC actions or flags affected and the platforms verified. Maintainers cannot run automated regression on every PR.

8.7 Potential improvements

Area Current state Improvement
Sandbox Disabled (snap incompatibility) Re-enable per-platform once snap supports it
Test coverage Zero Add Spectron / Playwright Electron tests for IPC flows
Update integrity Relies on platform code signature Add per-release manifest signature (e.g. minisign / cosign)
Plugin sandboxing None beyond file:// gate Per-plugin permission model
Crash reporting Not configured Wire crashReporter.start() to a self-hosted endpoint for opt-in telemetry
Modularity One 2900-line electron.js Split into modules by concern (CLI, IPC, updater, security)
URL params overlay urlParams.json from cwd Move to userData to avoid working-directory injection vectors

8.8 Contributing flow

  1. Fork jgraph/drawio-desktop.
  2. Clone with --recurse-submodules.
  3. nvm use && npm install && npm run sync && npm start to verify the baseline works.
  4. Branch, make focused changes (avoid unrelated edits to the upstream submodule pin).
  5. Manual-test on at least the platform you care about.
  6. PR against master. Include screenshots / log output for UI changes.
  7. Editor-side changes — file the PR against jgraph/drawio instead.

9. Integration Points

drawio-desktop is end-user software, but its CLI surface and file format make it useful inside automation pipelines.

9.1 CLI export integration

The headless export pipeline (drawio --export ...) is the canonical integration. Every output format produced by the upstream web app is reachable from the CLI.

Use case Invocation
Embed in CI for diagram → PNG/SVG/PDF xvfb-run drawio --export --format svg -o out/ --recursive diagrams/
Convert legacy Visio drawio --export --format png input.vsdx
Generate static viewer HTML drawio --export --format html --html-edit-link <url> input.drawio
Per-page screenshot drawio --export --format png --page-index 3 input.drawio
Multi-page PDF with embedded XML drawio --export --format pdf --all-pages --embed-diagram input.drawio

Tip

The HTML export emits a self-contained interactive viewer with optional zoom, lightbox, layer/tag panels, and an Edit button (--html-edit-link). Embed the resulting HTML in static documentation sites for read-only diagram delivery without requiring drawio-desktop on the reader's machine.

9.2 File-format integration

.drawio files are XML (typically zlib-compressed and base64-encoded). The mxfile root tag is documented in the upstream jgraph/drawio repository. For programmatic generation, treat drawio-desktop as the canonical renderer rather than a parser — your code emits .drawio XML, the wrapper renders it.

.drawio is also embeddable inside other formats:

  • PNG with embedded XML — drawio-desktop writes a zTXt chunk with key mxGraphModel containing the deflated XML. readPngXml() (in electron.js:1820) extracts it on import.
  • PDF with embedded XML — for PDF 1.7, the wrapper attaches diagram.xml as an embedded file with mime application/vnd.jgraph.mxfile. readPdfXml() (line 1897) reverses this on import.
  • SVG with embedded XML — the content attribute of the SVG root carries the URL-encoded XML; readSvgXml() extracts it.

9.3 OS file association integration

The standard OS handlers ensure double-click and drag-to-icon flows work without configuration. For applications that want to launch drawio-desktop programmatically, prefer the OS-native protocol (open on macOS, start on Windows, xdg-open on Linux) over hardcoding the binary path:

# macOS
open -a "draw.io" diagram.drawio

# Linux
xdg-open diagram.drawio

# Windows (cmd)
start "" "diagram.drawio"

9.4 Plugin integration

Plugins are JavaScript files loaded into the renderer at editor boot when --enable-plugins is set. The plugin runs with the same window.electron bridge as the core app, meaning a plugin can call any of the umbrella rendererReq actions. Plugin authors should:

  • Not assume window.process or require() exist (they do not — contextIsolation: true).
  • Use the upstream draw.io plugin API (documented at the upstream repo) rather than poking the wrapper directly.
  • Keep state in electron-store keys with a unique prefix to avoid collision with the wrapper's own keys.

9.5 Auto-updater integration in derivatives

If you fork drawio-desktop for redistribution, you have three options for updates:

  1. Re-target the GitHub provider at your own repo by editing electron.js:1365-1369 — change repo and owner.
  2. Switch to a self-hosted feed by setting provider: 'generic' and a url in setFeedURL. You then host latest.yml and the installers yourself.
  3. Disable the updater by patching disableUpdate.js to return true.

9.6 What drawio-desktop does not integrate with

For honesty's sake, here is what is not present despite being common in similar apps:

  • No telemetry / analytics — no Google Analytics, Sentry, Mixpanel, or any other phone-home.
  • No cloud sync — no Dropbox, Google Drive, OneDrive, GitHub, GitLab, or Trello backends in the desktop default URL query (each is explicitly set to 0). Cloud features remain in the upstream web app but are disabled in the desktop bundle.
  • No collaborative editing — single-user, local-file-only.
  • No remote command execution — connect-src 'self' pins the renderer to bundled origin.
  • No external font loading by default — Google Fonts is opt-in via isGoogleFontsEnabled.

This is by design: drawio-desktop's value proposition is offline, local-first, no-account diagram editing.