GenerateSaaS

Development and builds

Run the desktop app in development against your local backend, then package per-OS installers with electron-builder.

Running in development

In dev the app reads the monorepo root .env - the same file the web app uses - so it targets whatever backend your architecture runs (a fullstack frontend serving /api on its own port, or a separate backend on its own port). With the web app running, start the desktop app with no extra env:

pnpm --filter desktop dev

This runs electron-vite dev: it starts the renderer dev server with HMR, builds the main and preload bundles, and opens the native window.

  • Overrides: BASE_URL / PUBLIC_API_URL set in your shell still override the .env values (e.g. to point dev at a staging backend).
  • Renderer: loads from the electron-vite renderer dev server with hot-reload; the main process points the window at it via ELECTRON_RENDERER_URL.

Building installers

Package a platform's installers with its build script - each runs electron-vite build then electron-builder:

pnpm --filter desktop build:mac    # or build:win / build:linux
PlatformBundles
macOS.dmg (+ a .zip the updater consumes)
Windowsnsis (.exe)
Linux.AppImage + .deb

electron-builder targets the host OS, so build each platform's installer on that platform (or in CI). Output lands in apps/electron/dist/.

When AI ships, every packaged build first stages the companion into the app: pnpm run stage:companion (wired into each build:* script) builds the standalone companion and copies its vendored Node runtime plus the daemon into apps/electron/resources/companion/, which electron-builder bundles. The packaged app runs that bundled copy directly - there is nothing to install separately. A build with AI off skips this step and ships no companion.

  • Signing: macOS code signing and notarization are secret-gated; Windows uses Azure Trusted Signing when configured, otherwise it builds unsigned - see Releasing.
  • Packaging config: apps/electron/electron-builder.mjs (targets, asarUnpack, files, the macOS/Windows/Linux blocks). The app identity - appId, productName, deep-link scheme, update feed - is read straight from @repo/config/desktop, so the installer and the running app can never disagree.

The in-app terminal uses a native node-pty binding. electron-builder rebuilds it to Electron's ABI in its beforePack hook (the build has npmRebuild off), so the packaged binding always matches the Electron runtime across platforms and arches - a belt-and-suspenders step, not a manual one.

To rebrand the app icon, replace apps/electron/build/icon.png with your own 512x512+ PNG; electron-builder generates the per-OS icons (.icns, .ico) from it at build time.

On this page