vitrinka.docs

Licensing

Obtaining, installing and renewing an enterprise licence — a vendor-signed file verified offline against the key built into the server.

What a licence is

A licence is a small JSON file signed with the vendor's Ed25519 key and issued per customer. Every official release carries the vendor's public key compiled in and trusts exactly that key, so the file is verified offline inside the server process — no activation call, no licence server, no phone-home, nothing that breaks in an air-gapped network. It carries the customer name, the tier, a seat count (0 = unlimited), whether air-gap bundles are entitled, and the issue and expiry dates. The seat count is written to the boot log for reconciliation; it is not enforced at runtime and is never reported anywhere.

There is no self-serve channel: licences are signed by the vendor. Contact your vendor to obtain one or to renew — your bundle typically arrives with the licence already in place.

Installing

Drop license.json into the bundle's ./license/ directory; Compose mounts it read-only at /run/license and points LICENSE_FILE at it (outside the bundle the default is DATA_DIR/license.json). Placement and the backup gotcha (the licence is not in /data) are covered in Operations.

  • No file — vitrinka runs in an unrestricted evaluation posture; the licence machinery is a no-op. The hosted service runs this way.
  • A valid file — the boot log says license active, with the customer, tier, seats and expiry.
  • A file the server cannot use — the boot refuses and the process exits with license file <path> refused: <cause>. Fix: <fix>. A broken licence never silently degrades to unlicensed.
Cause in the refusalWhat to do
its signature does not verify against this build's license keyThe file was edited, damaged in transit or signed by someone other than the vendor: put back the license.json your vendor issued, unedited, or ask for it to be re-issued.
not a well-formed license fileReplace it with the file your vendor sent, byte for byte — a licence is not hand-editable.
issued in the future — the server clock is behindSet the host clock right (NTP) and restart; the licence itself is fine.
cannot read itMake the file readable by the server process.
this build carries no license public keyRun an official vitrinka release; this build cannot verify any licence.

Every refusal but the clock one also names the evaluation way out: remove the file to run unrestricted.

Check the installed file offline with the server binary itself — it verifies against the key built into it and prints the payload with expired and inGrace, or the refusal the boot would give. When the server has refused to boot, docker compose run --rm --no-deps vitrinka license-inspect /run/license/license.json runs the same check without it:

docker compose exec vitrinka /vitrinka license-inspect /run/license/license.json

What it gates

Expiry is a soft freeze, never a shutdown. A licensed install sends X-License-Status on every response — the status word only, never licensee data:

StateBehaviour
ActiveEverything works. X-License-Status: active.
Expired, in graceEverything still works for 14 days past expiry; the app shows a banner asking for renewal and the boot log warns license expired — grace period running (X-License-Status: grace).
Expired beyond graceEvery write — any method but GET, HEAD and OPTIONS — answers 403 saying writes are paused until renewal; reads always work, and the banner says so (X-License-Status: expired). Sign-in, account recovery, health checks, the browser error tunnel and the licence status endpoint stay open, so an admin can always get in and renew; sign-up, adding a passkey and connecting GitHub wait for the renewal.

The transition happens live — a long-running process crosses expiry and grace without a restart.

GET /api/v1/license always answers 200, frozen or not, and is never cached (Cache-Control: private, no-store). A caller without a signed-in browser session — token callers included — gets only licensed and status, the same word as the header and all the banner reads. A signed-in browser session also gets customer, tier, expiry, expired and inGrace. On the host, license-inspect reads the full file without a session.

Never a hard shutdown, and never data loss — the freeze exists to be renewed out of, not to punish.

Renewing

Replace the file and restart the container; the licence is read at boot, so the freeze lifts as the server starts with the renewed file:

cp new-license.json ./license/license.json
docker compose restart vitrinka

On this page