Skip to main content

Overview

An Air-Gapped install is licensed with two sealed files, not one: Both files are issued together from the xpander backoffice, both name the same license_id, and the platform binds them to each other at runtime. The split exists so that renewal never touches the install file: to extend a term, raise a cap, or add a feature, xpander re-issues only a fresh activation.json. You swap it in and restart. The install file, your registry mirror, and everything the org seed created stay exactly as they are.
This is the xpander-license/v2 format. Installs issued under the older single signed license.json (xpander-license/v1) keep working through --license-file (see Legacy single-file license). New air-gap installs use the two-file flow described here.

Why two files

  • Renewal is cheap and low-risk. A term extension or a cap bump re-issues one small file. There is no reinstall, no re-mirror, no change to the org identity, and no risk of disturbing a running platform beyond a restart.
  • Identity is separated from entitlement. install.json answers who this install is (the org, the edition) and never expires, so a lapsed term can never wedge a helm upgrade or block a config change. activation.json answers what the install is entitled to right now (term, seats, agents, features) and is the only file that ages.
  • Both are opaque to the customer (see below).

Sealed and opaque

Both files are sealed: encrypted and unreadable by the customer, by design. Each file on disk is a small JSON envelope wrapping one opaque blob:
The sealed blob is produced by sign-then-seal: xpander signs the license payload with an Ed25519 private key (so the contents cannot be forged or edited), then seals that signed payload with libsodium crypto_box_seal (so the contents cannot be read). The platform images carry the seal key needed to open the blob and the Ed25519 public key needed to verify the signature. A customer sees only format, kind, and base64. Editing the sealed blob invalidates it, and the two kind values are not interchangeable (the installer and the runtime each reject a file presented in the wrong slot). Because the files are opaque, the installer cannot read the org id out of them. You pass it explicitly with --org (the installer prints a clear error if you omit it), and the consuming services verify at boot that the bound org id matches. The org id is shown in the backoffice at issuance as ORGANIZATION_ID.

Installing with the two files

The backoffice hands you both files plus a ready-to-run command. From a terminal with kubectl access to the target cluster:
The installer:
  1. Validates both envelopes structurally (each is xpander-license/v2, one is kind: install and the other kind: activation). It refuses two of the same kind, or the files swapped between flags.
  2. Takes the org binding from --org, since the sealed files hide it.
  3. Creates the xpander-license Secret with both files as keys (install.json and activation.json), and wires the chart values that point at them.
Everything else about the air-gap install is unchanged: registry mirroring, the data-tier choice, the hook pipeline, sign-in. See Air-Gapped Edition for the full install flow.
Both flags are required together. Passing only one exits with a usage error naming the missing flag. --org is required too, because the sealed files are opaque and the installer cannot read the org id from them.

Deploying the chart directly (Argo CD / Helm)

If you drive Helm yourself instead of the installer (GitOps, Argo CD, Flux), create the Secret with both keys and point the chart at them. Create the xpander-license Secret carrying both sealed files:
Then set the license values:
Setting installKey and activationKey selects the two-file (v2) mode. Leaving both empty falls back to the legacy single-file global.license.key (license.json). Do not set both modes at once.
The seal private key is baked into the platform images and does not need to be supplied. Only set global.license.sealKey when you are deliberately rotating the seal key with xpander (an unexpected value here means the platform cannot open the sealed files).

What the license enforces

Seat and agent caps

max_seats and max_agents in the activation file are enforced (an unset cap means unlimited).
  • Creating the N+1th user past max_seats, or the N+1th agent past max_agents, is refused, with a message explaining the cap and how to raise it (re-activate, see below).
  • Everything already created keeps working. Existing users sign in, existing agents run. A cap only blocks new creation beyond it; it never disables what already exists, even if a smaller activation file is later swapped in.

Expiry and grace

The activation file’s expiry places the install on a valid, expiring, grace, expired timeline: The key property: expiry blocks only the creation of new tasks. It never logs anyone out, never stops a running task, never takes the UI or your data offline. A lapsed term degrades to “read and finish what is running”, not “locked out”.
Feature grants (features, plus the features implied by the air-gap edition such as custom models, BYOK, SSO, and custom connectors) also come from the activation file. A feature the license does not grant is refused with a message pointing at re-activation; everything already enabled keeps working.

Renewing: re-activate

Renewal (extending the term, changing a cap, or adjusting features) re-issues only the activation file. In the backoffice (staff), the license row has a Re-activate action that issues a fresh activation.json for the same org and the same license_id, with a new expiry and caps. The install file is not reissued. On the install:
1

Get the new activation.json

From your xpander contact or the backoffice Re-activate action. It carries the same org_id and license_id as your current install.
2

Swap it into the Secret

Replace only the activation.json key; leave install.json untouched:
3

Restart the platform

Roll the license-consuming services so they re-read the file:
Or run bash install.sh upgrade. License consumers carry a checksum annotation over the Secret, so an upgrade rolls them onto the new activation file.
No reinstall, no re-mirror, no change to the org or its data. The new term and caps take effect once the services come back up.
The new activation file must name the same org and license_id as the install file. The platform binds the two at runtime and refuses an activation file that belongs to a different license.

Staff: issuing a license

From the xpander-app admin area (staff-only, air-gap customers), Issue a license produces, in one handoff view:
  • install.json and activation.json, two downloads.
  • The install command (the curl … | bash … line above), prefilled with the org id.
  • ORGANIZATION_ID, the generated org id the customer passes as --org.
  • The License public key, the base64 Ed25519 the customer wires into global.license.publicKey for a direct-chart deploy.
The Re-activate row action later issues a fresh activation.json only (same org, same license_id, new expiry / caps / features) for the renewal flow above.

Legacy single-file license

Installs issued before two-file licensing use one signed license.json (xpander-license/v1), installed with --license-file ./license.json and wired through global.license.key. That path still works and is documented under The License on the Air-Gapped page. New installs and renewals use the two-file flow on this page; the two modes are selected by whether installKey and activationKey are set.

Next Steps