GenerateSaaS

Companion setup

A desktop app with AI bundles a local companion daemon on the user's machine - one consent click, no download, and nothing to pair for local use. It runs while the app is open, or as a background service when the user turns that on, and can optionally connect to your backend.

A desktop app with AI bundles its own companion daemon. The daemon is staged into the packaged app at build time, so there is nothing for the user to download or install, and pairing is optional rather than required to run: the app runs it for each user as a local daemon on their own machine, starting it over loopback, supervising it while the app is open, and stopping it on quit. By default the daemon runs fully on-device - the user's own coding CLIs run there, driven by the daemon, and your backend stays out of the loop. The model turn is the one network hop a run makes - the user's CLI calls its own model provider directly, under the user's own login. (Opening the model picker also has the daemon fetch the public models.dev catalog so the list stays current; it is a static model list, sends nothing about the user or the machine, and a built-in fallback covers offline.) Connecting the daemon to your backend is an optional, explicit step the user takes (see Backend connection); disconnecting returns it to fully local.

The bundled daemon's source comes from generating (or updating) the project with the AI option on - not from the web companion option. config.desktop.agents.enabled then decides whether the app actually spawns and supervises it. The two are not the same switch: the app's AI source always ships, the daemon's does not.

If your project was generated without AI, flipping config.desktop.agents.enabled to true is not enough. It turns the screens on, but apps/companion was never added, so packaging stops at stage:companion with an error telling you to re-run the CLI (init or update) with --ai to add that source. Do that first, then flip the flag.

config.companion.enabled governs the web install flow (the Companions page, device pairing, the one-line installer); a desktop app with AI bundles and runs its own daemon whether or not you also offer that web feature, and this page applies in both. Publishing the daemon is only for the web install flow, where a user downloads and installs it themselves; the app's copy is bundled, so it needs none. The on-device confinement, the local policy ceiling, and the local audit log are the same ones the companion feature documents; this page is only the desktop app's side of it.

Before the daemon runs the first time, the app asks for consent. A card states exactly what running the companion on this machine means:

  • Their own coding CLIs run on this machine, driven by the companion - by default the app orchestrates every run locally and your backend sees none. (The CLI still reaches its own model provider over the network for each turn.)
  • Every run works inside the companion's work folder, under the permission ceiling they set - your server can never raise it or point a run somewhere else.
  • Every run is logged locally on this machine - they can review the full trail anytime with the daemon's log command.

Not now leaves the feature dormant and re-asks next time; Allow proceeds and is remembered. Once allowed, the app stages the on-device config and starts the local daemon - there is nothing else for the user to set up.

App-supervised, while the app is open

By default the app owns the daemon's lifecycle: it starts one on demand and tears it down when the app quits, so the companion runs only while the app is open. The daemon serves this one machine over loopback, and for local use needs nothing paired - pairing to your backend is the optional connection below.

One machine runs one daemon. Before starting its own, the app checks whether a daemon already serves this machine - the user's own always-on service, or a companion left from a prior session. When one already serves the local drive, the app attaches to it rather than fighting its single-instance lock, and starts its own only when none is running; on quit it stops only a daemon it started, never the user's service. If the daemon already running is an older, paired-only one that exposes no local drive to attach to, the app cannot attach and says so plainly - a clear "another companion holds this machine" state, not an empty screen - and it attaches on its own the moment that daemon begins serving the local drive (for a service, its next auto-update).

Schedules that must keep running while the app is closed are the one exception: the daemon can be installed as a background OS service. That service runs from a stable copy staged outside the app bundle, so an app update or move never strands it, and config or schedule changes reach the background daemon the next time the app opens. The background choice is surfaced in the app (see Schedules); an app that cannot report its background state says so rather than implying the service is off.

Backend connection (optional)

The bundled daemon boots local and never needs a backend. Connecting it to your product's backend is an optional, explicit action, surfaced beside Background mode: a Connect control the user chooses, states what it does, and can undo at any time. It is off until the user turns it on.

  • Connect pairs the bundled daemon with your backend, using the app's own signed-in session so the user approves it without a second browser round-trip. A connected daemon then runs your backend's scheduled and dispatched work here, beside the device's own local runs, and its schedules appear in the app as a live view alongside the local ones - nothing is copied, so they disappear again on disconnect.
  • Disconnect returns the daemon to fully local. It best-effort ends this device's backend session first, then removes the local pairing. If the backend cannot be reached, the local pairing is still removed and the device may remain in your account's device list until it is forgotten there.
  • A manual pair from a terminal on the same machine is equivalent to Connect, and the control reflects it: the card reads the daemon's live pairing state, so a pairing made or removed outside the app shows up the next time it reads.

A buyer can forbid this connection entirely with config.desktop.agents.connectionLock (see the structurally private product): with it on, the Connect control does not render and the app-managed daemon refuses to pair, serving local runs only. The lock binds the daemon the app manages; a separate always-on service the user installs and pairs themselves runs backends under the user's own authority, outside the lock. The web Companions surface, when your product ships it, manages your users' other devices; it does not manage this bundled in-app daemon, which the Connect control here owns.

What runs, and where

Everything the companion orchestrates stays on the user's machine, under limits they can see - with the model turn as the one call that leaves it (and, when connected, backend-origin runs composed by your backend but still executed here):

  • Their own CLIs (Claude Code, Codex, and the rest) run on-device under their existing logins - the app never holds a key or a session. Each CLI still calls its own model provider over the network to answer a turn.
  • A confined work folder bounds what a run may change; the policy ceiling bounds what it may do. This is a confinement, not a general secrets sandbox - the companion feature's trust surface states exactly what it does and does not guarantee.
  • A local audit log records every run before it executes.

On this page