Offline License Verification
Offline License Verification — ɳSelf documentation.
ɳSelf plugin licensing is online-first with a bounded offline path, not offline-first: a bundle
plugin install validates live against ping.nself.org by default, and only falls back to a
time-limited local cache if that call cannot complete. This page documents the real grace ladder,
fail-open variable, and offline commands, cited against the CLI source so it cannot drift out of
sync again.
Default behavior: bounded offline grace
nself plugin install checks bundle entitlement live (BundleEntitled,
cli/internal/license/checker.go:59-118). If the server answers, that answer is authoritative: a
401/403 or a valid: false response fails closed immediately, no cache involved. Only when the
call cannot complete at all, a transport error, DNS failure, or timeout, does the CLI fall back to
your local, signed license cache, and only on a bounded ladder
(bundleEntitledFromGrace, cli/internal/license/checker.go:168-196):
| Cache age since last successful validation | Result |
|---|---|
| Under 72 hours | Proceeds silently |
| 72 hours to 7 days | Proceeds, with a warning printed to stderr |
| Over 7 days | Fails closed — the install is refused until validation succeeds |
A revoked license is refused at any cache age; the ladder never overrides revocation. These are the
same numbers as the nSelf Bundle License §4 and the
Licensing page — one source (cli/internal/license/grace.go), three places it’s
read from.
Fail-open for CI and air-gapped installs
Set NSELF_LICENSE_FAIL_OPEN=1 to replace the bounded ladder above with an unbounded check, for CI
runners and air-gapped installs that are offline by design. It only changes the same
transport-failure branch: instead of the 72h/7d ladder, the CLI checks the cache’s tier against
what you are trying to install with no age ceiling at all
(bundleEntitledFromCache, cli/internal/license/checker.go:124-151). It is not a global switch —
a server that answers 401/403 or valid: false still fails closed regardless of this variable, and
a revoked license is still refused at any cache age:
FAIL-OPEN exception: when
NSELF_LICENSE_FAIL_OPEN=1is set (CI / air-gap environments) and the network is unreachable, falls back to the local cache to check tier coverage. Production deployments MUST NOT set this var.
Removing the age ceiling matters because the cache is a bare credential: it carries only the license key, with no per-machine identifier, so a copy of it works on any host it is placed on. The 72h/7d ceiling is what limits how long a copied cache stays useful; this variable removes that limit entirely.
| Variable | Default | Purpose |
|---|---|---|
NSELF_LICENSE_FAIL_OPEN |
unset (bounded 72h/7d grace) | CI/air-gap only. Never set on a production install. |
LICENSE_CACHE_PATH |
~/.cache/nself/license.json |
Override the license cache file location. |
Air-gapped installs: export / import
For a host that never reaches the internet, move a signed cache file over instead of setting
NSELF_LICENSE_FAIL_OPEN:
# On a machine that can reach ping.nself.org:
nself license refresh
nself license export > cache.json
# Copy cache.json to the disconnected host, then:
nself license import cache.json
nself license import verifies the Ed25519 signature on the file before writing it to the local
cache and refuses an unsigned or tampered file (cli/internal/license/validate.go:177-195).
NSELF_LICENSE_SKIP_VERIFY=1 (plus NSELF_LICENSE_SKIP_VERIFY_FORCE=1 and --force) can force
import to accept an unsigned file. Development and testing only — it defeats the signature
check that makes the offline cache trustworthy, so never set it on a production installation.
Manual refresh
nself license refresh
Pulls a fresh signed entitlement from ping.nself.org/license/validate and replaces the local
cache. Run this after extended offline periods, after key rotation, or before a planned offline
window.
Simulating offline behavior
LICENSE_ALLOW_SIMULATION=true nself license simulate-offline <days>
nself license simulate-offline --clear
Exercises the ladder by backdating your local cache so the CLI behaves as though it had been
offline for that many days. It is gated behind LICENSE_ALLOW_SIMULATION=true and returns
ErrSimulationNotAllowed without it (cli/internal/license/simulate.go:36-38).
This writes to the cache file — it is not a dry run. SimulateOffline backdates FetchedAt and
calls WriteCache (simulate.go:52-55), so run nself license simulate-offline --clear afterwards
to restore the real timestamp. Never run it against a production install.
Useful days, given the 72h/7d ladder above: 1 stays silent, 5 triggers the warning, 10 puts
the cache past the ceiling.
Troubleshooting
Cache corruption or a signature that no longer verifies. Remove the cache file at
LICENSE_CACHE_PATH (default ~/.cache/nself/license.json) and run nself license refresh. The
cache is fully regenerable from a live validation.
Stuck failing closed with no network. Use the export/import workflow above from a machine that
does have network access, or set NSELF_LICENSE_FAIL_OPEN=1 if this is a CI runner or air-gapped
install where that is the accepted trade-off — never on a production host.