Setting up your license key
Applying feature checks
The license service is queried in three places:
1. Middleware — server-side API guard
// src/routes/api/rdp.js
router.use(requireFeature('remote_desktop'));
requireFeature responds with 403 {ok:false, feature, upgrade_url} on missing feature. For quantitative limits there is requireLimit(key, countFn):
// src/routes/api/peers.js (schematisch)
router.post('/', requireLimit('vpn_peers', () => peers.count()), ...);
requireLimit distinguishes three states: -1 → unlimited, 0 → feature not available (403), otherwise comparison count >= limit.
2. Templates — UI gate
injectLicense makes license.features available in every view:
{% if license.features.remote_desktop %}
<a href="/rdp">RDP</a>
{% else %}
<span class="lock-icon" title="Pro feature">…</span>
{% endif %}
The UI typically shows locked features anyway (with a lock icon) to make upgrade paths visible.
3. Services — opportunistic
At the service layer, features are queried to skip side paths:
// src/routes/api/gateway.js (Heartbeat-Handler)
if (body.hostname && hasFeature('internal_dns')) {
peers.setHostname(peerId, body.hostname, 'agent');
}
The heartbeat itself does not fail when internal_dns is off — the field is simply ignored.
Enforcement on downgrade
When a license refresh detects a plan change (previousPlan !== cachedPlan), enforceLimitsInternal() runs:
- For every limit feature (
vpn_peers,http_routes,l4_routes), the actual count is computed. - If
count > limit, thecount - limitoldest entries (ORDER BYcreated_atASC) are set toenabled=0. - WireGuard config and Caddy config are synchronized — deactivated peers/routes disappear from the wire.
- Activity log entry
peer_license_disabled/route_license_disabled. - Optional email alert if
email_alerts_enabled=true.
Records are never deleted. A later upgrade does not automatically reactivate them, but the admin sees them in the UI as deactivated and can manually enable them.
Pattern for new features
For every new paid feature, four things must happen (see feedback_new_feature_license.md):
- Flag in COMMUNITY_FALLBACK — usually
false, as a limit usually0. - API guard —
router.use(requireFeature('X'))or per-route. - UI guard — template wrap with
{% if license.features.X %}and lock fallback. - Service fallback — on downgrade, no service must crash, only the feature code path is skipped.
Key rotation and removal
removeLicense() deletes the JWT, stops the refresh timer, sets community mode, removes the key from the DB, and starts enforcement. _overrideForTest(features) patches cachedFeatures directly — only for test setup, not for production.
Testing the licensing system
The service exports _overrideForTest and _getHardwareFingerprint for test harnesses. In test setup, the real refresh timer is not started (no startLicenseRefresh() call), so tests stay deterministic.
See also
- routing.md — HTTP/L4 routes are gated via
route_auth,rate_limiting, etc. - home-gateway.md —
gateway_peers,gateway_tcp_routing,gateway_wol. - rdp-routes.md —
remote_desktop,rdp_via_gateway. - internal-dns.md —
internal_dns. - ../API.md — HTTP response formats for 403 feature errors.
Source files
src/services/license.js— Core service,COMMUNITY_FALLBACK,validateLicense,enforceLimitsInternal.src/middleware/license.js—injectLicense,requireFeature,requireLimit,requireFeatureField.src/services/settings.js— DB-backed key storage for UI activation.config/default.js—license.server,license.tokenPath,license.key,license.signingKey.