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.jsonanswers who this install is (the org, the edition) and never expires, so a lapsed term can never wedge ahelm upgradeor block a config change.activation.jsonanswers 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: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 withkubectl access to the target cluster:
- Validates both envelopes structurally (each is
xpander-license/v2, one iskind: installand the otherkind: activation). It refuses two of the same kind, or the files swapped between flags. - Takes the org binding from
--org, since the sealed files hide it. - Creates the
xpander-licenseSecret with both files as keys (install.jsonandactivation.json), and wires the chart values that point at them.
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 thexpander-license Secret carrying both sealed files:
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.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 pastmax_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 avalid, 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 freshactivation.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.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.jsonandactivation.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.publicKeyfor a direct-chart deploy.
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 signedlicense.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
- Air-Gapped Edition: the full install flow, registry mirroring, data tier, upgrades
- Air-Gapped Security Architecture: zero egress to xpander Cloud, the signed Ed25519 license, local sign-in

