Desktop Applications
Build native cross-platform desktop applications with BoxLang, Electron, and Vite — ship a full JVM web server inside every installer.

BoxLang desktop applications combine three things you already know: BoxLang server-side logic running inside a local MiniServer, an Electron shell that owns the native window, tray, and menus, and a Vite build pipeline for modern frontend assets. The result is a cross-platform desktop app that ships a real HTTP server — not Electron's renderer process playing fetch tricks — so your BoxLang code runs exactly the same way it does on the web.
We provide a turnkey starter to get you moving immediately:
Everything the starter gives you out of the box:
BoxLang MiniServer running inside the desktop app on a local port
Electron shell with app menu, system tray, global shortcuts, and native window lifecycle
Vite build pipeline for JS and SCSS assets (Alpine.js + Bootstrap 5 included)
SQLite datasource pre-configured via
Application.bxFull packaging flow for macOS, Windows, and Linux
📋 Table of Contents
🏗️ Architecture
The architecture separates three concerns that each own their layer:
Desktop shell
Electron
Window, tray, menus, shortcuts, native OS integration
Application server
BoxLang MiniServer (Undertow)
BoxLang template and class execution
Frontend assets
Vite + Alpine.js + Bootstrap
JS, SCSS, HMR in dev / hashed bundles in production
Architecture diagram
Runtime flow

Electron starts from
app/electron/Main.js.Main.jswires the modular components:BoxLang,AppMenu,TrayMenu, andShortcuts.BoxLang.jsspawns the MiniServer process using the settings inminiserver.json.BoxLang.jspolls the server URL until it is reachable, then tells theBrowserWindowto load the local server address.Electron renders the BoxLang application inside the desktop window as if it were a browser tab, but with full native OS integration.
Config relationship diagram
Java 21+ must be installed on every machine that runs the desktop app. Only the BoxLang MiniServer binaries and libs are packaged inside the installer — no JRE is bundled.
📋 Prerequisites
Java 21+ — required on every machine that will run the app
BoxLang CLI — install with the Quick Installer or the BoxLang Version Manager (BVM)
Node.js 25+ — for Electron and Vite
CommandBox — optional but useful for BoxLang dependency management (
box install)
⚡ Quick Start
1. Get the starter
The starter lives at https://github.com/ortus-boxlang/boxlang-starter-desktop-electron. You have two options:
Option A — Use as a GitHub Template (recommended for new projects)
Click the "Use this template" button on the GitHub repository page to create your own repository pre-populated with all the starter files, then clone your new repo:
Option B — Clone directly
2. Install dependencies
3. Package the MiniServer runtime
The starter ships without a pre-built MiniServer. Run this once to download and extract it from the version pinned in .bvmrc:
This downloads MiniServer into runtime/bin and runtime/lib. The version is read from .bvmrc:
Use npm run package:miniserver:force to re-download over an existing runtime.
4. Start development mode
This launches Vite (with HMR on 127.0.0.1:3000) and Electron in parallel. Electron waits for Vite to be ready before starting the MiniServer and loading the window.
📁 Project Structure
Where developers usually edit
UI pages and templates
public/
BoxLang business logic and models
app/
Frontend JS behavior and styles
resources/assets/
Native window, tray, menu, shortcuts
app/electron/
Server config (port, rewrites, etc.)
miniserver.json
Runtime debugMode, cache, mappings
.boxlang-dev.json / .boxlang.json
⚙️ Configuration
miniserver.json — server control
miniserver.json — server controlThis is your primary runtime control file during development. It tells BoxLang.js how to start the MiniServer:
port
Port for the local MiniServer — pick something that won't conflict with other apps
host
Bind to 127.0.0.1 so the server is only reachable from localhost
webRoot
Folder served as the web root (public/ by default)
serverHome
BoxLang home directory (modules, config, compiled classes)
rewrites
Enable URL rewriting for clean URLs
envFile
Path to a .env file loaded into the MiniServer environment on boot
Keep host set to 127.0.0.1. Binding to 0.0.0.0 would expose the local server to the network, which is not appropriate for a desktop application.
.boxlang-dev.json vs .boxlang.json
.boxlang-dev.json vs .boxlang.jsonThe BoxLang runtime reads different config files for development and production so the two environments stay predictable.
.boxlang-dev.json — used during npm run dev:
.boxlang.json — used in the packaged/distributed app:
The /app and /public mappings are present in both files. They let your BoxLang code reference classes and templates by mapping path regardless of where the app is installed on the user's machine.
Keep both files under version control. They define the application's class resolution and runtime behavior and should ship with the project.
.bvmrc — MiniServer version pin
.bvmrc — MiniServer version pinA single-line file containing the MiniServer version to download:
Update this when you want to upgrade. Then re-run npm run package:miniserver:force to fetch the new version.
🌐 BoxLang Web Layer
Everything inside public/ is served by the MiniServer. This is where you write BoxLang templates, classes, and helpers.
public/Application.bx
public/Application.bxThis is the BoxLang application descriptor. It runs once when the app boots and defines application-level settings, mappings, and datasources:
Change this.name, datasource config, mappings, and lifecycle methods here as your application grows.
public/index.bxm
public/index.bxmThe default landing page. It uses Bootstrap 5 and Alpine.js and calls ViteHelper to include the correct asset URLs:
public/includes/helpers/ViteHelper.bx
public/includes/helpers/ViteHelper.bxResolves asset URLs based on the runtime environment:
Development (
ENVIRONMENT=development): points directly to the Vite dev server on127.0.0.1:3000for Hot Module Replacement.Production: reads the Vite manifest (
public/includes/resources/.vite/manifest.json) and returns the correct hashed file URLs.
You never call this directly beyond what is already in Application.bx. onApplicationStart instantiates it once into application.viteHelper.
🎨 Frontend Layer
Source files live in resources/assets/:
Build output goes to public/includes/resources/ (git-ignored — generated by Vite).
Vite development
When you run npm run dev, Vite starts on 127.0.0.1:3000 with HMR. The ViteHelper.bx detects the ENVIRONMENT=development flag injected by concurrently and points asset tags at Vite directly.
Production build
Outputs hashed bundles into public/includes/resources/ and writes manifest.json. The ViteHelper.bx reads the manifest in production to serve the correct file names.
Bootstrap 5 and Alpine.js are installed as regular npm packages and bundled by Vite — you are never loading them from a CDN, which keeps the desktop app fully offline.
🖥️ Desktop Layer
All desktop behavior lives in app/electron/. Each module has a single responsibility:
Main.js — bootstrap and window lifecycle
Main.js — bootstrap and window lifecycleStarts Electron, sets up logging to the OS log directory, creates and manages the BrowserWindow, and wires together all modular components:
Electron resources:
BoxLang.js — MiniServer process manager
BoxLang.js — MiniServer process managerHandles the full lifecycle of the MiniServer child process: startup, readiness polling, crash recovery, restart, and graceful shutdown.
Key behaviors:
Prefers the packaged
runtime/bin/boxlang-miniserverexecutable; falls back to globalboxlang-miniserveronPATH.On Unix/macOS: automatically sets execute permissions if they are missing.
Polls the server URL at 500 ms intervals until it responds or the 30-second timeout expires.
On crash: waits 5 seconds then restarts (suppressed if
isQuittingis true).Graceful stop is triggered on
app.before-quit.
Node.js resources:
AppMenu.js — native application menu
AppMenu.js — native application menuDefines the menu bar shown on macOS and inside the window on Windows/Linux. Extend this to add your own menu items and keyboard accelerators.
Electron resources:
TrayMenu.js — system tray
TrayMenu.js — system trayCreates the status icon in the system tray (macOS menu bar, Windows notification area, Linux status bar) with a context menu to show, hide, restart, or quit the app.
Electron resources:
Shortcuts.js — global keyboard shortcuts
Shortcuts.js — global keyboard shortcutsRegisters global shortcuts that fire even when the app window is not focused.
Electron resources:
💻 Development Workflow
Scripts reference
npm run dev
Start Vite + Electron in development mode (HMR enabled)
npm run start
Start Electron only (assumes Vite dev server is already running)
npm run build
Build frontend assets into public/includes/resources/
npm run prod
Build assets then start Electron in production mode
npm run preview
Preview the Vite production build in a local server
npm run lint
Lint JS files with ESLint
npm run lint:fix
Auto-fix lint errors
npm run generate:icons
Regenerate app icons from a source PNG
npm run package:miniserver
Download MiniServer from .bvmrc into runtime/
npm run package:miniserver:force
Force re-download even if already present
npm run package
Build assets and run electron-forge make for all platforms
npm run package:mac
Build macOS distributions only (--platform darwin)
npm run package:win
Build Windows distributions only (--platform win32)
npm run package:linux
Build Linux distributions only (--platform linux)
npm run package:linux:docker
Build Linux distributions via Docker (for cross-platform builds)
npm run package:full
Package MiniServer then build all distributions
Typical development loop
Edit BoxLang templates in
public/— changes are picked up immediately since MiniServer re-processes templates on every request.Edit SCSS or JS in
resources/assets/— Vite HMR pushes changes to the Electron window instantly.Edit Electron modules in
app/electron/— you need to restart Electron (Ctrl+Cthennpm run devagain) for JS changes to take effect.
BoxLang templates have no compile-restart cycle. Save the file and refresh — that is all.
✏️ Coding Your Application
Adding pages and templates
Create .bxm files anywhere under public/. The MiniServer serves them as BoxLang templates:
With rewrites enabled, http://127.0.0.1:59700/dashboard maps to /dashboard.bxm.
Adding BoxLang classes
Place classes in app/ or public/includes/. The /app mapping makes everything under app/ reachable:
Create it in a template with new /app/models/RecordService() or add it to the application scope in onApplicationStart:
Using the SQLite datasource
The starter wires up a local SQLite database at .database/boxlangDB (the folder is created automatically). Use queryExecute anywhere in your templates or classes:
Customizing the native menu
Edit app/electron/AppMenu.js. Add your own items using Electron's Menu.buildFromTemplate() API:
Adding a global keyboard shortcut
Edit app/electron/Shortcuts.js. Register with globalShortcut.register:
Customizing the tray menu
Edit app/electron/TrayMenu.js. Add items to the contextMenu template array:
Application name and app ID
Edit forge.config.cjs in the packagerConfig section:
Also update this.name in public/Application.bx.
⚙️ Electron Forge
The starter uses Electron Forge as its build and packaging toolchain. Forge replaced the older electron-builder workflow and provides:
A single
electron-forge makecommand that packages, makes, and signs artifacts in the correct orderFirst-class maker plugins for every platform (DMG, Squirrel, DEB, RPM, Flatpak, ZIP)
A built-in publisher system for uploading artifacts to GitHub, S3, and more
Hooks for injecting custom post-build logic at any step
The config lives in forge.config.cjs (CommonJS format — required by Forge):
asar must remain false. BoxLang.js spawns runtime/bin/boxlang-miniserver as a real filesystem executable — enabling asar archiving would break that path lookup entirely.
Platform makers
The starter ships makers for every supported platform:
maker-dmg
macOS
.dmg
Primary macOS distribution format
maker-pkg
macOS
.pkg
Alternate installer; only included when MAC_SIGNING_IDENTITY env var is set
maker-squirrel
Windows
.exe + win-unpacked/
No-admin, no-prompt Squirrel installer
maker-zip
All platforms
.zip
Universal fallback; used for auto-update distribution and CI archiving
maker-deb
Linux
.deb
Debian / Ubuntu
maker-rpm
Linux
.rpm
RHEL / Fedora (Linux hosts only)
maker-flatpak
Linux
Flatpak bundle
Sandboxed; skipped when SKIP_FLATPAK=1
postMake hook
postMake hookAfter every build, the postMake hook automatically copies three helper files into each ZIP artifact:
scripts/mac-open.sh
Shell script to bypass macOS Gatekeeper on unsigned builds
scripts/win-unblock.ps1
PowerShell script to unblock unsigned Windows apps
scripts/UNSIGNED-BUILD.md
Instructions for users who receive an unsigned build
These helpers ensure users always have the workaround at hand when distributing unsigned CI artifacts, without having to find them in docs.
📦 Building and Distributing
Build assets only
Produces hashed JS and SCSS bundles in public/includes/resources/ and writes the Vite manifest. This step is required before packaging.
Package for all platforms
This runs in sequence:
npm run package:miniserver— downloads and extracts the BoxLang MiniServer intoruntime/.npm run build— compiles frontend assets.electron-forge make— packages and signs the app for the current host platform.
Package for a specific platform
Installers land in dist/electron/:
macOS
.dmg, .pkg (if MAC_SIGNING_IDENTITY is set), .zip
Windows
.exe (Squirrel installer), .zip
Linux
.deb, .rpm, Flatpak bundle, .zip
Electron Forge produces platform-specific artifacts. To build a macOS .dmg you must be on macOS. Use CI with a matrix build — for example, GitHub Actions with macos-latest, windows-latest, and ubuntu-latest runners — to produce all three platforms from a single pipeline.
Updating the MiniServer version
Edit
.bvmrcto the desired version number.Run
npm run package:miniserver:force.Rebuild with
npm run package:full.
🔐 Code Signing
Unsigned applications trigger security warnings on both macOS (Gatekeeper) and Windows (SmartScreen). Electron Forge handles signing and notarization at the correct build step automatically once credentials are configured.
Code signing is a prerequisite for auto-updates on macOS. Without a valid signing identity, macOS blocks auto-update payloads entirely.
macOS
macOS requires two layers: code signing (certifies the author's identity) and notarization (Apple's automated malware scan, mandatory since macOS 10.15 Catalina).
Prerequisites
Purchase a membership in the Apple Developer Program.
Obtain a Developer ID Application certificate (for distribution outside the Mac App Store).
Install it into your keychain via Xcode.
Verify it is installed:
security find-identity -p codesigning -v
Configuring forge.config.cjs
forge.config.cjsAdd osxSign and osxNotarize to packagerConfig. Both are already stubbed in the starter — supply credentials via environment variables:
Never store credentials in plaintext in forge.config.cjs. Always supply them as environment variables or use a stored keychain profile.
Alternative osxNotarize authentication options:
The starter's forge.config.cjs already conditionally includes maker-pkg (for .pkg output) when MAC_SIGNING_IDENTITY is set as an environment variable in CI.
Windows
Windows signing is applied to the installer artifact at the Make step.
Prerequisites
Since June 2023, private keys must be stored on FIPS 140 Level 2+ hardware storage modules. Software-based OV certificates are no longer available for purchase.
Install Visual Studio (free Community Edition is sufficient) to get
signtool.exe.
Configuring forge.config.cjs
forge.config.cjsThe maker-squirrel config already accepts certificate settings via environment variables:
Set WIN_CERT_FILE (path to your .pfx file) and WIN_CERT_PASS in your CI environment or a local .env file that is excluded from version control.
Azure Trusted Signing (modern cloud alternative)
Azure Trusted Signing is Microsoft's cloud-based signing service and the most cost-effective option for eliminating SmartScreen warnings. Available to US/Canada organizations with 3+ years of verifiable business history.
🔄 Auto Updates
Electron Forge integrates with Electron's built-in auto-update API. The recommended approach depends on your distribution model.
A signed application is required for auto-updates on macOS. Configure code signing before enabling auto-updates.
Open source apps (GitHub)
Open source desktop apps hosted on GitHub can use the free update.electronjs.org service:
Configure the GitHub Publisher in
forge.config.cjs.Install the
update-electron-apppackage:
Call it at startup in
app/electron/Main.js:
Static storage (S3)
If you use the S3 publisher, refer to its documentation for configuring the app to auto-update from uploaded artifacts.
Self-hosted update server
For private apps where you need more control (percentage rollouts, multiple release channels):
@electron-forge/publisher-nucleus
GitHub publisher
Electron Release Server publisher
GitHub publisher
🐛 Debugging
Electron apps have two separate processes, each with its own debugging approach.
Renderer process (Chromium DevTools)
Open DevTools from inside the running app:
Keyboard shortcut:
Ctrl+Shift+I(Windows/Linux) orCmd+Option+I(macOS)App menu: View → Developer Tools (registered by
AppMenu.js)
Main process — command line
Use the --inspect-electron flag when starting via Forge:
Then open chrome://inspect in any Chromium-based browser and click inspect next to your app to attach a debugger. Use --inspect-brk-electron to pause at the very first line of execution.
Main process — VS Code
Add a launch configuration to .vscode/launch.json:
Open the Run and Debug view (Ctrl+Shift+D), select Electron Main, and press F5 to start debugging with full breakpoint support.
BoxLang template debugging
Enable
"debugMode": truein.boxlang-dev.jsonfor stack traces in BoxLang template output.Check the MiniServer log piped to the Electron terminal for request errors.
📤 Publishers
Publishers take the artifacts produced by electron-forge make and upload them to a distribution service. Configure them in the publishers array of forge.config.cjs:
Run publishing with:
Available publishers
GitHub Releases
@electron-forge/publisher-github
Open source; pairs with update.electronjs.org for free auto-updates
Amazon S3
@electron-forge/publisher-s3
Private distribution + S3-hosted auto-updates
Electron Release Server
@electron-forge/publisher-electron-release-server
Self-hosted update server
Nucleus
@electron-forge/publisher-nucleus
Full-featured self-hosted update + release management
Bitbucket
@electron-forge/publisher-bitbucket
Bitbucket-hosted distribution
All publishers default to publishing artifacts for all platforms. Add a platforms key to restrict which platform artifacts a specific publisher uploads.
🌍 Cross-Platform Considerations
Java 21+
Must be installed separately on every target machine — the installer does not bundle a JRE
Executable permissions
On macOS/Linux, BoxLang.js automatically runs chmod +x on the MiniServer binary at startup if needed
Paths
Always use path.join() in Electron code — never string concatenate paths directly
Icons
Supply .icns (macOS), .ico (Windows), and .png (Linux) variants; use npm run generate:icons to regenerate from a source PNG
App ID
Use a reverse-domain identifier (e.g. com.example.myapp) for proper OS registration
Port
Pick a port above 49152 for the local MiniServer to avoid conflicts with system services
🔧 Troubleshooting
Server fails to start
Run
npm run package:miniserverto ensureruntime/binandruntime/libexist.If using global fallback, verify
boxlang-miniserveris on yourPATHwithwhich boxlang-miniserver.Confirm the port in
miniserver.jsonis not in use:lsof -i :59700.
Permission denied on macOS/Linux
Run
npm run package:miniserver:forceto re-extract with correct permissions.Manually fix with
chmod +x runtime/bin/boxlang-miniserver.
Missing production assets
Run
npm run buildand confirmpublic/includes/resources/.vite/manifest.jsonexists.Check the Vite build for errors in the terminal output.
App works in dev but fails after packaging
Confirm Java 21+ is installed on the target machine.
Open the packaged app's log file (macOS:
~/Library/Logs/<AppName>/main.log; Windows:%APPDATA%\<AppName>\logs\main.log) for startup errors.Verify that
asar: falseis set inforge.config.cjsso the runtime files are accessible to the child process.
BoxLang template errors
Enable debug mode in
.boxlang-dev.json("debugMode": true) to see stack traces in the BoxLang output.Open Electron DevTools with
Ctrl+Shift+I(or the View → Developer Tools menu item) to inspect the page.
📚 Resources
BoxLang
Electron
Frontend
Last updated
Was this helpful?
