Desktop app
Ship a cross-platform Electron desktop app, gated by config.desktop - enable it, configure it, and brand it.
A cross-platform Electron app for desktop-first products. The native app is your primary surface; the web frontend keeps handling marketing, purchase, authentication, and billing. Gated by config.desktop and ships off by default.
| Location | apps/electron |
| Stack | Electron + React 19 + Vite (electron-vite) + shadcn/ui on Base UI |
| Theme | shared @repo/styles |
| Web frontend handles | marketing, purchase, authentication, billing |
The app is two parts: a main process (apps/electron/src/main) that owns the native window, deep links, updates, and secure storage; and a React renderer (apps/electron/src/renderer) that draws the UI and reaches the main process through a narrow, validated preload bridge (window.api). The renderer never touches native APIs directly. When AI ships, the app also bundles a local companion daemon inside the packaged app and supervises it, running every AI action on the user's machine.
The desktop app requires a Pro (or Agency) license. Solo licenses cannot scaffold apps/electron: the CLI hides the option and the license server rejects it.
Enabling it
Scaffold with the desktop app via the --desktop flag on the init command:
generatesaas init --desktopThe flag defaults off; --no-desktop is the explicit off.
What --desktop controls, depending on whether it is on or off:
| Piece | --desktop on | --desktop off |
|---|---|---|
apps/electron (Electron main + React renderer) | kept | stripped |
bearer plugin in @repo/auth | kept | stripped |
/auth/device verification page + deviceAuthorizationClient wiring in your frontend | kept | stripped |
deviceAuthorization plugin in @repo/auth | kept | kept (inert) |
config.desktop block in @repo/config | enabled: true | kept, enabled: false |
The off-state keeps the deviceAuthorization plugin, deviceAuthorizationClient wiring, and config.desktop block because they are additive and inert without the app, which avoids fragile surgery on the shared Better Auth config.
The inert deviceCode database table and the device.* i18n keys are kept for every project regardless of the flag - they are harmless and avoid surgery on the generated Better Auth schema and translations.
See Configuration for how flags map to the generated config.
What the app includes
Two more answers at init - AI, and the companion - shape what the desktop app is:
- Desktop app with AI. The app ships the local AI stack (Chat, Schedules, and the AI Settings tabs - models, tasks), the Terminal that runs the user's own coding CLI, and a self-branded companion daemon bundled inside the packaged build. It also offers Background mode: the daemon can install as an OS service so schedules keep firing while the app is closed. By default every AI action runs on the user's machine, with the user's CLI calling its own model provider as the only network hop; the user can optionally connect the app to your backend to also run its scheduled and dispatched work here, and disconnect to return to fully local.
- Desktop app with AI + the companion. The companion option adds the web side of the same daemon - the Companions install page, the device cards, and the pairing endpoint your users reach from the browser. The desktop app already bundles and runs the daemon without it; turning the companion on lets your web users install and pair one too.
- Desktop app without AI. A client shell: login, onboarding, home, account, settings, and organization, wired to your backend over device sign-in. No AI pages, no Terminal, no bundled daemon - the marketing and AI surfaces stay on the web. The AI source is still in the project, held off by
config.desktop.agents.enabled: false, so you can turn the surface on later without re-scaffolding; the daemon is the exception - it arrives only by re-runninginit/updatewith AI on.
A structurally private product is a fourth shape from the same answers: ship the desktop app with AI on, hide the web AI surface with config.ai.enabled: false, and forbid the bundled daemon's backend connection with config.desktop.agents.connectionLock: true. These are the two halves of the private posture: config.ai.enabled: false keeps AI off the web, and connectionLock keeps the desktop daemon fully local - the app renders no connect control and the daemon refuses to pair, on both sides. connectionLock is config-only and defaults to false (the connection capability ships enabled); a privacy-product buyer opts in. Your users get the full local AI stack in the native app - on-device runs, their own CLIs, their own model logins - while your website exposes no AI at all. The desktop app's AI reads config.desktop.agents.enabled (a desktop project derives it from the AI option), so it stays on even with the web surface hidden.
Entitlements and billing
The app reads the session plan and credits and can gate features behind them.
- Plan and credits source: the pure-data
@repo/config/pricingexport plus the renderer'sapps/electron/src/renderer/lib/entitlements.ts. - Billing stays on the web: the Account screen links out to
/settings/billingrather than embedding a checkout.
Next steps
Sign-in and security
Browser-based device authorization sign-in and the shell's security boundaries - CSP, the preload bridge, and the OS keychain.
Organizations
How the app honors config.tenancy - org switching, role-gated management, per-entity entitlements.
AI agents
Turn the user's own AI subscription or API key into a workspace-scoped agent.
Companion setup
The companion ships inside the app and runs as a local daemon, consent-first - no separate install, and pairing is optional.
Development and builds
Run against your local backend, then package per-OS installers with electron-builder.
Releasing
Build, sign, notarize, and publish from GitHub Actions - automatically after CI passes on main, or manually.
Updates and downloads
Background auto-updates via electron-updater and stable always-latest download links.