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, as you would a firewall rule.
An app has no 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.
What follows from that#
| 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. |
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:
| 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 works only when the agent runs on the router. Off-router it has no access to the
router's filesystem, so it says the folders have to be created on the router before the
change is applied, and Advanced mode shows the mkdir -p command — see
On-router, off-router, fleet. On the router it creates only folders
that do not exist yet, and none outside /config/.
Lifecycle is configuration too#
There is no control socket, and the router's HTTP API has no restart operation for a
container. The router's CLI has exactly one — restart container <name> — and nothing for
start or stop. So:
| Action | What it actually is |
|---|---|
| Stop | Stage set container name <app> disable |
| Start | Stage a delete of that disable node |
| Restart | Two operations. POST /api/containers/{name}/restart runs the router's own restart container and changes no configuration — nothing staged, nothing committed, no revision written — and answers 501 off-router, because the wrapper only exists on the box. POST /api/apps/restart is the two-commit bounce: commit the disable, then commit its deletion, both audited and both in the history. A stopped app refuses with 409 either way, and if the bounce's 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 not removed on uninstall, so a reinstall picks up where it
left off.
The update answer says either 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:
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. Never an app-specific API call.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.
Hints stage; they never apply. The Commit Bar is still the only thing that commits.
Feature modules: a catalogue entry with no container#
Three of the 40 catalogue entries are kind: "builtin" — WireGuard, Suricata IDS and
UPnP / NAT-PMP. They are features whose Wheelhouse page is gated on being installed.
- 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. - Configuration outranks the flag. If the feature is configured on the router — by the
CLI, by another operator, by anything — the page shows regardless. 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 not an app. No credible published miniupnpd container was found; the
candidates were unaudited personal builds, which is not acceptable provenance for a
privileged host-network daemon on a firewall, and the base has no service upnp
configuration node or miniupnpd package underneath. The agent answers UPnP and NAT-PMP
itself instead: UPnP / NAT-PMP in the catalogue is a built-in feature, set up on the
UPnP page, and the Companions page reports it.
Wheelhouse does not drive an app's own API. The health probe is a TCP connection plus an optional HTTP path, never an app-specific call, and no app's credentials are held here. The one exception is deliberately narrow: a catalogue entry may declare a stats URL and a path to a single number in the JSON it answers with, and the app's card shows that number with its age. Two entries declare one; the rest of the catalogue shows what it always showed.
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.