GenerateSaaS

Companion Daemon

Let your users run their own AI subscriptions for your app, 24/7, from hardware they control - point at an existing shared companion like OpenCompanion, or publish your own branded daemon.

The companion feature lets your users put their own subscription coding CLIs (Claude Code, Codex, opencode, Hermes Agent) to work on your app, around the clock, from a machine they control - a laptop, a home server, a VPS. Your app dispatches work to a small terminal daemon the user installs; the user keeps and pays for their own AI subscription, and their keys and source never leave their machine.

It is the user-owned counterpart to the external agent (MCP): the MCP server lets a third-party agent reach into your app, while the companion daemon runs on the user's own hardware and reaches out to your app over authenticated HTTP.

The companion is off by default. It only works when config.companion.enabled is true. With it off, the /companion/* routes are absent entirely and the Companions tab is hidden. The companion also requires the AI product to be on (config.ai.enabled) - it drives the same capability layer the assistant uses.

One feature, two sources

The daemon your users install can come from two places - you choose at init (companionSource) and can switch later.

Shared (default)Vendored
The daemonAn existing public companion repo - OpenCompanion by default, or any companion built with the same pipelineIts source lives in your repo (apps/companion), branded with your name
MaintenanceNone - the shared repo's owner cuts releases; installed daemons auto-update from itYours - you publish releases with one command and own the repo, cadence, and issues
BrandingThe shared companion's identity (your app renders its install command)Your brand end to end - binary name, service label, env vars
Repo weightLight - no daemon source, no export pipeline, no coding-CLI engine in your installsThe daemon + export pipeline vendor in; pnpm install pulls the coding-CLI engine (~212MB)
Best forMost projects - the shared daemon covers standard dispatch needsProducts that customize device-side behavior or need their own brand in front of users

Either way, everything else is identical: pairing, the Companions page, dispatch, policy, and audit all live in your app and are documented on install & pairing.

Choosing at init

With the companion enabled, init asks for the source - shared is preselected with OpenCompanion prefilled, vendored is one keystroke away. Non-interactive flags: --companion-source <shared|vendored> and --companion-repo-url <url> (shared), or a companion name prompt (vendored). See init.

Switching later

Run generatesaas update and ask your AI assistant to switch the companion source. Both directions are supported:

  • shared -> vendored vendors apps/companion + the export pipeline, branded from your chosen name. The Companions page shows a publish-first state until your first pnpm companion:publish.
  • vendored -> shared removes apps/companion and the export pipeline. Local customisations to the vendored daemon are abandoned (the update confirms this first), and your users keep running your old published daemon until they install the shared one.

What your app controls

Everything you configure stays in your app; the daemon carries only its brand identity.

  • config.companion.enabled - the master switch. Off removes the /companion/* routes and hides the Companions page.
  • config.companion.clientId - the Better Auth device-authorization client_id the daemon pairs with (default "companion", distinct from the desktop app's "desktop" client).
  • config.companion.pollIntervalMs - the idle poll cadence handed to the daemon (default 10s, clamped to [1s, 60s] so it stays under the 90s presence TTL). The backend speeds it up on its own while a run is live or the user has your app open, and the transport holds each companion to a per-route request budget. See poll cadence and request budgets.
  • config.companion.daemon - the daemon identity (name, binary, env prefix, repo + install URLs) the Companions page reads to render the install command. Shared: resolved from the companion repo's brand.json at shape time. Vendored: mirrored from your apps/companion/brand.json and kept in lockstep by a guard test.
  • The dispatch surface. Your app queues a run; the daemon polls GET /poll, runs it locally, and pushes result frames to POST /events, which stream live to the dashboard viewer over SSE. When the agent calls one of your capabilities, the daemon calls back POST /tool-call and your app resolves it.
  • Capability composition. The daemon runs whatever your capability layer declares - the same tools your in-app assistant and scheduled runs use. The tools you declare are exactly what the agent can call: you compose the agent's abilities server-side, and the daemon injects them into each run. Tools you declare on the agent yourself reach a device too - see Your Agent on a Companion for the one constraint they must satisfy.

One install, several accounts

A user who signs in to your app under more than one account - a personal login and a work login, or one seat per client - pairs each of them from a single daemon install on the same computer. Every pairing keeps its own bearer, connected CLIs, policy ceiling, and work folder, so a run for one account can never read another's files. Commands take --user <id> beside --url to pick the account, and existing installs re-key themselves at the first boot after upgrading. See several accounts on one machine.

Set up from the desktop app

If your product also ships the desktop app, it runs the companion for each user as a local daemon on their own machine - consent-first, with no terminal, no pairing, and no code to copy. It attaches to any daemon already serving the machine - including the user's own always-on service - and starts its own only when none is running, so one daemon serves both the app and any paired backend. It supervises that daemon while the app is open and, on quit, stops only the one it started - never the user's service. See Companion setup.

A wired CLI session for any user

Any user can open their own Claude Code or Codex against your product, from their own terminal, with your capabilities on the CLI's MCP surface and your instructions in its context: <binary> terminal --url https://your-app.com/api. Your backend composes the session; the daemon runs it on the user's machine, in your product's work folder or in one of their own they granted. See Terminal Sessions.

Dispatching device-side work from your code

Your product code can dispatch runs to a user's companion with dispatchCompanionRun from @repo/api/companion - gated by a per-device user grant, with status reads and audit attribution. See Dispatch from Your Code.

On this page