Licence#
Administration → Licence shows what this agent holds, what the licence server last said about it, and takes a new key. The page's own subtitle states the rule the whole design follows:
Configuration changes need a licence; reading never does.
The page is admin-only. GET /api/license needs the viewer role — everyone can see the
state — while setting and removing a key are adminOnly.
What the licence gates#
Exactly one thing: the write plane. requireLicense wraps every configuration-
changing route, and an agent without a usable licence answers 402 Payment Required with
a message that names the fix rather than just refusing —
agent/license.go.
| Still works, unlicensed | Refused with 402 |
|---|---|
| Signing in, including single sign-on | Staging, committing and confirming |
| Every read: interfaces, firewall, NAT, DHCP, DNS, routing, apps, logs | Direct configure |
| The audit log and the commit history | Rollback, config/save and config/load |
Download config and GET /api/config/raw | Every app action — plan, pull, prepare, update, restart, feature, image delete |
/metrics | Reconcile |
| Account management, tokens, agent settings | Packet capture, which answers 501 anyway |
Three routes sit outside that split. A traceroute is operator-gated and not licence-gated: it runs a command and changes no configuration. Power and boot images are admin-gated and not licence-gated either — they are not configuration, and an unlicensed router can still be rebooted and still be upgraded.
That split is deliberate: losing sight of your router because a card expired would be a worse product than losing the ability to change it. It also means a lapsed subscription still lets you take your configuration off the box.
The seven states#
The badge in the header is the state, and the tone tells you how bad it is.
| State | Writes | Meaning |
|---|---|---|
valid | open | Verified, and today is before the expiry. |
grace | open | Expired, but inside the grace period. The banner names the date writes stop. |
clock-unverified | open | The router's clock reads before the licence was issued. |
expired | closed | Past expiry and past grace. |
revoked | closed | The licence server said so. |
invalid | closed | The stored key does not verify, and the page says why. |
unlicensed | closed | No key stored. |
An error banner sits under the header whenever the state carries a message: amber when the licence is still working (grace, clock-unverified), red when it is not.
This agent#
| Row | What it is |
|---|---|
| Licence | The licence id from the key. |
| Plan | The plan name the key carries. |
| Licensed to | Name and email from the key, when present. |
| Routers | The router limit; 0 is shown as unlimited. |
| Features | The feature list the key carries — fleet is the one the product gates on. |
| Expires | The date, and the days remaining. |
| Grace until | The date writes stop after expiry. |
| This router | The fingerprint — see below. |
The fingerprint#
A short hex string identifying this installation to the licence server so the router
limit can be counted. It is the first eight bytes of a SHA-256 over /etc/machine-id
(falling back to the host name when that file is empty), plus a fixed suffix.
It is stable across restarts and not across reinstalls. A rebuild onto new hardware is a new fingerprint, and the server counts it as an additional activation against your limit. The key keeps working; releasing the retired activation is done by whoever issued the key, and there is no button on the router for it — Backup and restore.
Licence server#
| Row | What it is |
|---|---|
| Server | The server the agent refreshes against, or refresh disabled when --license-server is empty. |
| Last refresh | Relative time, or not yet. |
| Server said | The status the server returned, and its message. |
| Refresh error | The last transport-level failure, or none. |
The panel's own summary, verbatim:
The key is verified offline. While the server is reachable the agent refreshes daily and the expiry follows the subscription; unreachable, it keeps working until the expiry plus grace.
How verification works#
A key is a signed statement: WHL1.<key id>.<payload>.<signature>. The payload is JSON
naming the licence id, the customer, the plan, the features, the router limit, the issue
time, the expiry and the grace period. The signature is Ed25519, verified against public
keys compiled into the agent binary.
That is the whole check, and it is offline. A router with no route to the internet still knows what it holds. A build can carry more than one public key, which is how rotation works: ship the new key beside the old one, then retire the old one on the server. A build with no keys is a build that cannot be licensed — every key is refused, rather than every key being accepted.
The refusal messages name the cause: "that is not a Wheelhouse licence key" for a bad
shape, "the licence signature does not verify", or "the licence was signed by a key
this build does not know (<id>); upgrade the agent".
How refresh works#
The agent posts the key, the fingerprint, the host name and its version to
/v1/refresh on the licence server. First attempt 20 seconds after start, then daily; a
failed attempt retries hourly.
| Server answer | What the agent does |
|---|---|
200 with a new token | Adopts it, stores it, and the expiry follows the subscription. |
403 | Marks the licence revoked. Writes close at once, even though the token still verifies. |
400, 402, 404, 409 | Records the refusal and its message; the current token stays good until expiry plus grace. |
| Unreachable, or any other status | Records the error. Nothing changes. The current token stays good. |
That last row is the important one. A network failure never costs you the write plane; only a definite "no" from the server, or the passage of time, does.
Entering a key#
Paste it into the box and press Activate. The button enables only when the text
starts with WHL1., and the agent verifies the signature before storing anything — a key
that does not verify is refused with 400 and the reason, and nothing is saved.
On success the key is stored in the agent's state file, an audit entry license-set is
written, and the agent immediately asks the server about it so the page can show the
answer.
The field's hint says where a key comes from: "Starts with WHL1. From your purchase email or the licence server."
A key can also be supplied at start-up with --license-key-file — the shipped unit
points it at /config/wheelhouse/license-key — or with --license-key. A key given by
flag is also saved, so the next start without the flag still holds it.
Removing a key#
Remove… opens a red panel that asks you to type the licence id to confirm, or the
word remove when the stored licence is too broken to have a readable id. The same
standard as reboot and power off: typing the name of the thing you are
about to affect.
The panel says what it costs:
Removing the key disables every configuration change on this router until another key is entered. Reading keeps working. Keep a copy of the key first — this page cannot show it again.
That last sentence is literal. The page never displays the stored key, and neither does
GET /api/admin/settings, which blanks it. Removal writes a license-remove audit
entry.
Licensed features#
The key can carry a feature list, and one feature is gated on it today: fleet.
requireFeature("fleet", …) refuses the fleet endpoints with 402 and the message "the
fleet feature is not part of this licence's plan", and the Fleet page is hidden from the
navigation for a licence that does not include it — a page whose entire content is "buy
the plan that turns this on" is an advert in the navigation of every single-router
install, and discovery belongs here instead.
The banner on every page#
The application shell reads the licence every five minutes and puts a line under the
header when there is something to say — no licence, a grace period running, or fewer
than eight days remaining. It is deliberately not confined to this page: the licence
gates writes everywhere, so a problem with it belongs everywhere —
ui/src/components/Layout.tsx.
What is not settled#
See also#
- Agent settings — where the key is stored, and why it is never echoed.
- System — audit —
license-setandlicense-remove. - Diagnostics — time — the clock, and
clock-unverified. - Backup and restore — the fingerprint after a rebuild.
- Roles — the other half of the authorisation model.
- What Wheelhouse costs — the product site's account of the model.
Checked against agent/license.go,
ui/src/pages/License.tsx,
ui/src/components/Layout.tsx,
ui/src/components/nav.ts,
agent/admin.go, agent/main.go,
packaging/wheelhouse-agent.service,
PRICING.md.