Apps are configuration#
Wheelhouse's answer to plugins is a container declared in the router's configuration
tree. Installing an app stages set container name <app> … operations into the
working set; you review the diff and commit it, exactly as you would a
firewall rule. Nothing about an app has a lifecycle of its own: stopping it is a
set … disable, starting it is a delete of that node, uninstalling it deletes the
subtree, and a rollback takes the apps with it.
Why that is the whole idea#
An install that is configuration inherits every property configuration already has.
| Property | Because |
|---|---|
| You see what it will do first | The install renders as set commands before anything is staged. |
| It is atomic | One commit, not a package script with steps that can half-finish. |
| It is reversible | rollback 1 restores the configuration that declared the app. |
| It survives an image upgrade | /config is the router's persistent partition. |
| It is isolated | Each app is a container with its own namespaces; host access is explicit and labelled in the catalogue entry. |
The comparison with a package-manager plugin, drawn honestly in both directions
including where the other model is ahead, is in
docs/apps.md.
Installing is three steps, and only one is configuration#
The router validates a container at commit and refuses it if either prerequisite is
missing, so both happen before the diff exists
(docs/apps.md, agent/apps.go):
| Step | Endpoint | Why it is not configuration |
|---|---|---|
| 1 · Create the volume source directories | POST /api/apps/prepare | The router answers Volume "conf" source path "…" does not exist at commit. Making a directory is an action. |
| 2 · Pull the image onto the router | POST /api/apps/pull | The router answers Image "…" does not exist locally at commit. |
| 3 · Stage the install | POST /api/apps/plan, then staging | This is the only part that is configuration. |
Step 1 only works when the agent runs on the router. Off-router it has no access
to the router's filesystem, so it hands back the exact mkdir -p command rather than
pretending to have done something — see
On-router, off-router, fleet.
Lifecycle is configuration too#
There is no control socket, and the router's API has no restart operation for a container. So:
| Action | What it actually is |
|---|---|
| Stop | Stage set container name <app> disable |
| Start | Stage a delete of that disable node |
| Restart | A two-commit bounce: commit the disable, then commit its deletion. Both commits are audited. A stopped app refuses with 409, and if the second commit fails the error says the app is down, not "restart failed". |
| Uninstall | Delete the whole container name <app> subtree |
| Update | POST /api/apps/update re-pulls the image and compares the local image ID before and after |
Volumes under /config are deliberately not removed on uninstall, so a reinstall
picks up where it left off.
The update answer is careful about what it knows: already current, or a newer image is on the router but the running container still holds the old one — restart to pick it up. A pull is not presented as an upgrade.
What makes it a plugin rather than a container#
Each catalogue entry declares what the UI should do once the app is running
(agent/catalog.go):
nav_label/nav_icon— the app gets its own left-nav entry under Installed, with a dot when it is declared but not running.web_ui— an Open link built from the container's real address.health— a TCP probe of the port, plus an HTTP request when a path is given. Deliberately not an app-specific API call: the product does not claim knowledge of an app's internals it does not have.hints— an offer surfaced on another Wheelhouse page, with the app's real address substituted in and the exact operations attached.GET /api/apps/hints/{page}is what a page asks for.
A hint's operations may carry four placeholders, filled from the router the hint is
offered on: {{address}} (the container's address), {{dhcp.network}} and
{{dhcp.subnet}} (the router's first DHCP scope), and {{dns.domain}}
(system domain-name, else its first authoritative zone). A router that cannot fill
one keeps the advice and loses the commands, and the card says what is missing. The
catalogue itself never names a network or a domain, and a test refuses one.
Hints stage; they never apply. The Commit Bar is still the only thing that commits.
Feature modules: a catalogue entry with no container#
Two of the 38 catalogue entries are kind: "builtin" — WireGuard and Suricata IDS.
They are native router features whose Wheelhouse page is gated on being installed,
which is the plugin model applied to something the kernel already has.
- Installing one is instant and stages nothing. There is no image, no volume, no
commit. The flag lives in the agent's own state under
/config/wheelhouse, because it is a surface preference rather than router behaviour (agent/apps.go,handleAppFeature;agent/store.go, theFeaturesfield). - Configuration outranks the flag. If the feature is configured on the router — by
the CLI, by another operator, by anything — the page shows regardless. Wheelhouse
never hides configuration that exists. Uninstalling with live configuration present
hands the
deleteoperations back for review and the page stays until they land.
What to know before you rely on it#
UPnP is deliberately absent. Registry checks found no credible published miniupnpd container — every candidate was an unaudited personal build, which is not acceptable provenance for a privileged host-network daemon on a firewall. The gap stays open and the Companions page says so.
Per-app dashboard widgets are not built, deliberately: the health probe is TCP plus an optional HTTP path and never an app-specific API.
See also#
- The configuration tree — where
container name …lives. - Staging: the working set — where an install lands.
- What the licence gates — every app route is a write.
- Apps — the store, in the manual.
- Apps — installed — what an installed app reports.
- Integration offers — hints, on the pages they affect.
Checked against#
docs/apps.md ·
agent/apps.go ·
agent/catalog.go ·
agent/catalog.json ·
agent/store.go ·
agent/main.go ·
README.md