Wheelhouse docs

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, unlicensedRefused with 402
Signing in, including single sign-onStaging, committing and confirming
Every read: interfaces, firewall, NAT, DHCP, DNS, routing, apps, logsDirect configure
The audit log and the commit historyRollback, config/save and config/load
Download config and GET /api/config/rawEvery app action — plan, pull, prepare, update, restart, feature, image delete
/metricsReconcile
Account management, tokens, agent settingsPacket 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.

StateWritesMeaning
validopenVerified, and today is before the expiry.
graceopenExpired, but inside the grace period. The banner names the date writes stop.
clock-unverifiedopenThe router's clock reads before the licence was issued.
expiredclosedPast expiry and past grace.
revokedclosedThe licence server said so.
invalidclosedThe stored key does not verify, and the page says why.
unlicensedclosedNo 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#

RowWhat it is
LicenceThe licence id from the key.
PlanThe plan name the key carries.
Licensed toName and email from the key, when present.
RoutersThe router limit; 0 is shown as unlimited.
FeaturesThe feature list the key carries — fleet is the one the product gates on.
ExpiresThe date, and the days remaining.
Grace untilThe date writes stop after expiry.
This routerThe 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#

RowWhat it is
ServerThe server the agent refreshes against, or refresh disabled when --license-server is empty.
Last refreshRelative time, or not yet.
Server saidThe status the server returned, and its message.
Refresh errorThe 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 answerWhat the agent does
200 with a new tokenAdopts it, stores it, and the expiry follows the subscription.
403Marks the licence revoked. Writes close at once, even though the token still verifies.
400, 402, 404, 409Records the refusal and its message; the current token stays good until expiry plus grace.
Unreachable, or any other statusRecords 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#


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.

Updated 2026-09-02 manual licence administration