ADR-0010: kaged supports per-user and system-wide deployment, equally

  • Status: Accepted
  • Date: 2026-05-21
  • Deciders: @karasu
  • Supersedes:
  • Superseded by:

Context

Earlier ADRs assumed a single deployment shape: kaged installed system-wide, running as a dedicated kaged system user, with state under /var/lib/kaged, fronted by an OAuth sidecar, reachable via a tunnel. That shape works for shared infrastructure (a homelab box several people use, a self-hosted operator console in a small team).

It does not work cleanly for the most common case: an individual operator running kaged on their own laptop or workstation, for their own use, without root, without a shared system user. For that operator, the system-wide model is unnecessary friction:

  • They don't want to sudo to install kaged.
  • They don't want a system service running for them when they're logged out.
  • They don't want to set up an OAuth sidecar to reach their own daemon from their own browser.
  • They want their state under $HOME, version-controlled by their own dotfiles if they like.

A separate but adjacent realization: projects should be shareable. An operator who creates a kaged project should be able to give the project to another operator, on a different machine, and have it work. That's not possible if the daemon's deployment model is one-size-fits-all and project files have to assume specific paths, users, or service shapes.

The question is load-bearing because it determines:

  • Where state lives on disk (ADR-0005).
  • What the auth model looks like in the absence of a sidecar (ADR-0007).
  • Which systemd unit the operator installs.
  • Whether project files can refer to operator-specific paths (they can't, if portability matters).

Decision

kaged supports two deployment modes as equal first-class citizens: per-user (the operator's own daemon, running under their UID, no root required) and system-wide (a shared kaged instance running as a dedicated system user). Both modes use the same binary, the same code paths, and the same project file format. Mode is selected at install/startup by the operator, not by the project.

Concretely:

Per-user mode

  • Binary location: anywhere on the operator's PATH. Often ~/.local/bin/kaged or /usr/local/bin/kaged.
  • State location: $XDG_DATA_HOME/kaged (default: ~/.local/share/kaged).
  • Config location: $XDG_CONFIG_HOME/kaged/config.toml (default: ~/.config/kaged/config.toml).
  • Process: the operator's own UID. No kaged system user.
  • Service: ~/.config/systemd/user/kaged.service enabled via systemctl --user enable --now kaged.
  • Default bind: 127.0.0.1:<random-free-port> (or a fixed port the operator configures).
  • Default auth: loopback + cookie-bound nonce (see ADR-0007 amendment). No sidecar required.
  • Tunnel/remote access: optional; the operator adds Cloudflare Tunnel + OAuth sidecar themselves the same way as in system-wide mode.

System-wide mode

  • Binary location: /usr/local/bin/kaged.
  • State location: /var/lib/kaged (as before).
  • Config location: /etc/kaged/config.toml.
  • Process: a dedicated kaged system user.
  • Service: /etc/systemd/system/kaged.service enabled via systemctl enable --now kaged.
  • Default bind: 127.0.0.1:7777.
  • Default auth: sidecar contract (as ADR-0007 originally described).
  • Tunnel/remote access: the blessed deployment with Cloudflare Tunnel + OAuth sidecar.

Mode detection

The daemon detects mode at startup by inspecting:

  1. Whether KAGED_HOME is set explicitly (operator's call wins).
  2. Whether the daemon is running under UID 0 or as the kaged user (system-wide indicators).
  3. Whether $XDG_DATA_HOME/kaged or ~/.local/share/kaged exists (per-user indicators).
  4. Falls back to per-user mode if ambiguous.

The detection is logged at startup so operators can see what was chosen.

What does NOT differ between modes

These are the same regardless of mode:

  • The project DSL format. Per ADR-0011, the DSL contains nothing operator-local. A project file from a per-user kaged works on a system-wide kaged and vice versa.
  • The HTTP+WS API surface. Same endpoints, same shapes.
  • The plugin SDK. Plugins target the same JSON-RPC contract.
  • The sandbox mechanism. bwrap works the same as a regular user as it does as a system user (modern user-namespace kernels). Per ADR-0009, bwrap is the v0 cage mechanism.
  • The CLI commands. kaged status, kaged dsl validate, etc. behave identically.

The mode is a deployment concern, not a behavior concern. Operators reading project documentation should not have to know which mode it was written for.

Consequences

What this commits us to

  • Two systemd units shipped as documentation: a system unit and a user unit. Both maintained.
  • XDG Base Directory Specification compliance for per-user paths. This is the operator-friendly convention on Linux; following it means dotfiles tools and disk-usage analyzers work as expected.
  • Mode-aware logging and error messages. "Where's my data?" has a different answer in each mode; the daemon must surface the active mode in kaged status and in startup logs.
  • Documentation that walks the operator through choosing a mode. Most likely a one-page "choose your deployment" doc that points at the two install paths.
  • Test matrix doubles in the deployment-touching layer. Storage paths, systemd integration, and auth defaults all get tested in both modes.

What this forecloses

  • No root-only privileges anywhere in the daemon. If we add a feature that requires root (e.g. binding port 80 directly, writing to /etc), it's not available in per-user mode. Per-user is the operator's UID; the operator can't escalate from inside kaged.
  • No global system state that per-user mode can't replicate. If a system-wide deployment carries something a per-user can't, the project file becomes mode-coupled and shareability dies. So: no global state.
  • No "the daemon needs to know which mode every other daemon is in." Cross-daemon coordination (a v2 concern) cannot assume one mode or the other. The protocol stays mode-agnostic.

What becomes easier

  • First-run UX for individual operators. Install via cargo install-style command (when we have one) or curl | sh (when we have one), run kaged start, open the printed URL. No sudo, no sidecar.
  • Shareability of projects. Combined with ADR-0011, a project file is portable from one operator's per-user kaged to another's system-wide kaged.
  • Trying kaged on a laptop. No reason to install a daemon and a sidecar just to evaluate the product.
  • CI and dev. Tests can spin up a per-user kaged in a temp directory without root, without systemd.

What becomes harder

  • Two filesystem layouts to document, test, and maintain. Some bugs will manifest in only one mode. We carry that.
  • Two auth defaults to document. Per-user is loopback-only by default; system-wide expects a sidecar. The "should I have a sidecar?" question has two right answers depending on mode.
  • Per-user mode complicates "shared homelab" stories. Two operators on the same Linux box each running their own kaged is supported (each is just a UID; XDG paths don't collide), but the social setup of "we share a Pi" now requires picking the model deliberately rather than defaulting.
  • systemd --user units have their own gotchas. lingering must be enabled for the user if they want kaged to run when logged out (loginctl enable-linger <user>). Documented; not invisible.

Alternatives considered

Alternative A — System-wide only

Why tempting: Less surface area. One filesystem layout. One auth path.

Why rejected: Drives away the largest population of likely operators — individuals using kaged for their own projects on their own machines. Forces them to deal with system users and sidecars they don't need. Violates the manifesto's "self-hosted by an individual operator" framing.

Alternative B — Per-user only

Why tempting: Simpler than two modes.

Why rejected: Excludes the "team homelab" and "small group running a shared kaged" cases. These are real and have different security needs (audit log per-operator, RBAC eventually) than per-user. A system-wide deployment is also the right shape for some kinds of hosted (operator-self-hosted, not SaaS) deployments where kaged should outlive any one operator login session.

Alternative C — Pluggable filesystem layout, no opinions

Why tempting: "It's just paths in the config."

Why rejected: Without opinionated defaults per mode, the operator has to make N micro-decisions about paths, users, units, auth — each compounding the install effort. The two named modes are useful precisely because they bundle decisions. Operators who want full custom can still set every path explicitly; the defaults exist for everyone else.

Alternative D — Single mode, but the install script branches

Why tempting: Internally only one daemon model; the difference is just where files live and which systemd unit ships.

Why rejected: That's almost what we're doing, but framing it as "two modes" makes the doc, the audit log, the status output, and the test matrix all clearer. The branches in code are small; the documentation clarity is large.

References