# drawio-desktop Administrator & Developer Guide

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

```text
+------------------------------------------------------------------+
| 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`:

```javascript
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`):

```javascript
{
  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.

```powershell
# 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`:

```bash
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](https://github.com/jgraph/drawio-desktop/issues/2219) for very recent Ubuntu releases.

### 4.5 Container / CI

```dockerfile
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:

```bash
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

```bash
drawio --version
```

```text
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`:

```text
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

```javascript
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

```javascript
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')`:

```javascript
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:

```javascript
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`):

```javascript
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`](https://github.com/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`](https://github.com/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:

```bash
# 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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
