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 pinsdefault-src 'self'to that origin, and every privileged action (file save, plugin install, dialog open) crosses an IPC boundary that validates the sender frame againstcodeUrl. Compromise of the renderer alone does not yield arbitrary code execution outside the bundleddrawio/src/main/webappdirectory.
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:
ipcMain.on('rendererReq', ...)— the umbrella dispatcher used for all file-system operations and most UI-bridging actions. Renderer callswindow.electron.request({action: 'saveFile', ...}, callback, error)and the main process routes byargs.action.- Direct
ipcMain.on(<event>, ...)handlers — high-frequency or stateful events such asexport,newfile,toggleSpellCheck,zoomIn,checkForUpdates,app-load-finished. - 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 checksstartsWith(codeUrl)wherecodeUrlis the on-diskfile://URL of the bundleddrawio/src/main/webapp/. Any cross-origin frame (an injected iframe, a navigated-awayBrowserWindow) fails the check and the handler returnsnullwithout 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=truesystem-wide, leaveisGoogleFontsEnabledatfalse(the default), and ensure nourlParams.jsonexists in the working directory. The CSP enforced by the main process pinsconnect-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
filesglob is["**/*", "!**/WEB-INF{,/**}"]for every config. TheWEB-INFexclusion matters because the upstreamdrawio/src/main/webapp/was originally a Java servlet bundle and still containsWEB-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-*exceptrelease-snapuse--publish always. Running them with noGH_TOKENfails the publish step but the artifacts still land in./dist/. Running them on a fork with a validGH_TOKENwill 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 extractedsquashfs-root/AppRundirectly 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.ymlon the GitHub Releases page after each tag push (automatic via--publish always) - [ ] Quarterly: audit
node_modulesfor vulnerable transitive deps (npm audit) - [ ] When upstream
jgraph/drawiocuts a new major:npm run sync, smoke test - [ ] Before each release: confirm all six
electron-builder-*.jsonconfigs 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-487against 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
- Quit the application (verify no
draw.io.exe/draw.ioprocess is running). - Back up
<userData>/config.jsonto a safe location. - Delete
<userData>/config.json. - Optionally clear
<userData>/Cache/and<userData>/Code Cache/(Chromium caches). - 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/environmentor/etc/profile.d/drawio.sh
Tip
On managed Windows endpoints, you can additionally remove the
Check for updatesmenu item from the macOS bundle by pre-settingdisableUpdate— the wrapper splices it out of the menu template at runtime whendisableUpdateis 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-pagesrather than per-page invocations — the wrapper combines pages in-memory viamergePdfs()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:219documents the reason: enablingapp.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-navigateis preempted — the renderer cannot navigate the page to a different URL.window.openis constrained to eitherabout:blank(withcontextIsolation: trueinherited) or external URLs throughopenExternal'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-desktoprepo write access could publish a maliciouslatest.ymlpointing at a malicious binary. For high-assurance fleets, disableelectron-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:
- Imports (lines 86-103) — Electron, fs, path, third-party modules.
- Store init (105-115) — instantiates
electron-storeinside try/catch; on failure,store = nulland the wrapper falls back to defaults silently. - Update kill-switch resolution (117-122) — combines
disableUpdate.jsreturn value, env var, CLI flag, and/.flatpak-infofilesystem probe. - Hardware acceleration switch (132-135) —
--disable-accelerationcallsapp.disableHardwareAcceleration()immediately. - Context-menu setup (138-157) —
electron-context-menuconfigured for spell check + paste-and-match-style. - Default URL query construction (182-216) — composes the parameters passed to
BrowserWindow.loadURL, optionally overlaid withurlParams.jsonfrom cwd. createWindow()(244-459) — used for every visible editor window. Handles size restoration, position bounds checking, the dirty-document close prompt, theCtrl+Shift+Vinterception.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).- App lifecycle hooks (1377-1491) —
before-quit,window-all-closed,activate,will-finish-launching(the macOSopen-filehandler),web-contents-created(navigation lockdown). - Auto-updater handlers (1493+) —
error,update-available,download-progress,update-downloadedevents, progress bar UI. - Helper functions for PNG metadata I/O, PDF combine, file content validation, draft management, plugin install, dialogs, font enumeration.
- The umbrella
ipcMain.on('rendererReq', ...)dispatcher (3232-3331) — switch onargs.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
npm install— installs runtime + dev deps; builds native modules if any.npm run sync—node ./sync.cjsupdates the upstreamdrawiosubmodule and copiessrc/main/webapp/into the location packaged into the asar.electron-builder --config <config>— packs source into asar (asar: true), runsafterPack: build/fuses.cjsto harden the binary, on macOS runsafterSign: build/notarize.mjs, on Linux/Windows produces native installers (NSIS/MSI/AppX/DEB/RPM/AppImage/snap).- Publish — with
--publish always, installers +latest.ymlupload to GitHub Releases via the GitHub provider, usingGH_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.storemay benullif init failed; every read usesstore != null ? store.get(...) : default. Every write is guardedif (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_SYNCwrites for diagrams. Save paths usefs.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.$viafsProm.renamerather 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
- Fork
jgraph/drawio-desktop. - Clone with
--recurse-submodules. nvm use && npm install && npm run sync && npm startto verify the baseline works.- Branch, make focused changes (avoid unrelated edits to the upstream submodule pin).
- Manual-test on at least the platform you care about.
- PR against
master. Include screenshots / log output for UI changes. - Editor-side changes — file the PR against
jgraph/drawioinstead.
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
zTXtchunk with keymxGraphModelcontaining the deflated XML.readPngXml()(inelectron.js:1820) extracts it on import. - PDF with embedded XML — for PDF 1.7, the wrapper attaches
diagram.xmlas an embedded file with mimeapplication/vnd.jgraph.mxfile.readPdfXml()(line 1897) reverses this on import. - SVG with embedded XML — the
contentattribute 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.processorrequire()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-storekeys 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:
- Re-target the GitHub provider at your own repo by editing
electron.js:1365-1369— changerepoandowner. - Switch to a self-hosted feed by setting
provider: 'generic'and aurlinsetFeedURL. You then hostlatest.ymland the installers yourself. - Disable the updater by patching
disableUpdate.jsto returntrue.
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.