GenerateSaaS

Updates and downloads

Background auto-updates via electron-updater, the per-OS update feed, and stable always-latest download links per platform.

Auto-updates

Packaged builds update through electron-updater. On launch the app checks the feed in the background, downloads a newer version, and installs it on the next quit; integrity is guaranteed by the installer's code signature and per-file .blockmap (no separate signing key to manage).

  • Feed: electron-updater reads <config.desktop.autoUpdate.url>/latest-mac.yml on macOS (latest.yml on Windows, latest-linux.yml on Linux). electron-builder bakes that URL into the packaged app.
  • Check: the Settings screen has a "Check for updates" control and an install action. A background download that finishes surfaces an "update ready" prompt; installing quits and relaunches into the new version.
  • Dev builds report updates as unavailable. To test the flow locally, apps/electron/dev-app-update.yml points electron-updater at a feed under electron-vite dev.

When AI ships, the bundled companion daemon updates with the app: an app update replaces the whole packaged bundle, the daemon included, so there is no separate daemon update to run. A background service re-stages from the updated bundle the next time the app opens. The companion's own <binary> update command is for a standalone, PATH-installed companion, not the bundled one.

Auto-update coverage is per platform:

PlatformUpdater artifactCoverage
macOS.zipfull auto-update (signed builds only)
Windowsnsis setupfull auto-update
LinuxAppImageAppImage only

Linux .deb users update through their package manager or by re-downloading, not through the in-app updater - only the AppImage carries the update feed.

Unsigned macOS builds cannot auto-update. Ship a signed macOS build (see Releasing) before relying on updates on that platform.

The download page

Your users get the app at /download - a public page your site already ships, linked from the footer. It lists one card per platform, each with the published version and its installer link.

The page reads the update feed, so a platform appears only once it has actually been released:

  • Before your first release, the feed does not exist yet. The page says "No release yet" rather than offering a link that 404s. This is what a freshly generated project shows, and it is correct - there is nothing to download until the release workflow has run.
  • After a release, each platform whose feed resolves gets a card with its real version. A platform you did not build is simply not offered.

The page ships only with the desktop app: with the app off, config.desktop.enabled is false, the route 404s and the footer link is hidden.

Every release publishes a stable, always-latest installer per platform under config.desktop.autoUpdate.url, named from config.desktop.protocol:

PlatformURL
Windows<autoUpdate.url>/<protocol>-Setup.exe
macOS<autoUpdate.url>/<protocol>.dmg
Linux<autoUpdate.url>/<protocol>.AppImage

To put a download button somewhere else (a navbar, a landing hero), @repo/utils/desktop-download builds those URLs for you:

FunctionReturns
fetchDesktopReleases()the published releases ({ platform, version, url }), empty when nothing is released yet - what the download page renders
desktopDownloadUrls()the per-OS map ({ mac, windows, linux }) of installer URLs, without checking the feed
desktopDownloadUrlFor(userAgent)the single installer URL for the visitor's OS, detected from the user-agent string (null when the OS is unknown)

desktopDownloadUrls() and desktopDownloadUrlFor() do not check the feed, so their URLs 404 until the first release exists - use fetchDesktopReleases() when a dead link would be visible to users. Call it from the server: the feed is a public bucket that sends no CORS headers. OS detection needs the browser, so read navigator.userAgent client-side with a fallback to listing all platforms.

Migrating an existing project to the Electron shell

The desktop app moved from Tauri to an Electron shell: the template now ships apps/electron and no longer carries apps/tauri. generatesaas update never deletes your files, so a project scaffolded before the migration keeps its old apps/tauri tree alongside the freshly-updated apps/electron. The update prints a one-line notice when it detects both.

  • Build and run the new shell (pnpm --filter desktop dev, then a packaged build) and confirm sign-in, updates, and any AI features work.
  • Then remove apps/tauri - the frozen shell is no longer built, released, or referenced.
  • Your config.desktop identity (appId, productName, protocol, feed URL) carries over unchanged, so installers and the update feed keep their names.

On this page