Skip to main content
An Air-Gapped install runs every xpander component in your environment, and nothing connects to xpander cloud at run time. It installs in a private cloud VPC (no internet or NAT gateways) or any disconnected environment.

What runs where

  • In your environment: the web UI, control plane, a self-hosted Supabase stack (auth, storage, edge functions) and the data tier (PostgreSQL, Redis Stack, MongoDB), as one Helm release in one namespace. Agent skill calls (including MCP) and agent workspaces run there too.
  • Sent to xpander: nothing. No heartbeats, no catalog sync, no telemetry.
  • Your choice: your LLM endpoint, cloud services such as Bedrock, S3 or SES, and the registry you mirror from at install time.
  • MCP clients (Claude, ChatGPT, Cursor) reach Omni once you set global.mcp.publicUrl. Until then, with ui.env.VITE_OMNI_MCP_URL empty too, Platform > Agents > Channels shows Not offered on this install, and the License page’s Capabilities card shows the value to add.
  • Private mode needs global.network.internetEgress: false. With true, built-in skills that use public APIs (for example arXiv and HackerNews) call them from the cluster. With false, those skills do not work.

Air-Gapped Security Architecture

Zero egress, the signed Ed25519 license, local sign-in, and the full install-time / run-time / never egress table.

Three things define an air-gapped install

  1. The license is the identity. Two sealed files (install.json and activation.json, format xpander-license/v2) plus the organization id you pass with --org. No console IDs and no API key. Older single-file license.json (v1) installs carry the org id inside. The installer generates the environment id and the first install creates your organization in-cluster. Single-tenant: one license, one organization.
  2. Images come from your own registry mirror. xpander delivers them through the gated xpanderaihub Docker Hub org with a per-customer read token. The installer mirrors them, or you mirror them yourself on a sealed network.
  3. Image tags are pinned per release. The chart refuses unpinned images, and an upgrade needs the new bundle mirrored first.
The interactive installer wizard runs with --edition airgap: a checklist, license validation, mirroring when needed, and a live watch over the install.
“Air-gapped” means from xpander’s cloud, not necessarily from the internet. Internet-backed skills (web search, docs search, TTS) can stay on or be sealed off. Ask your xpander contact about the sealed hardening profile.

What talks to what

Outbound. Nothing to xpander at run time. These are the connections you choose (full table: Air-Gapped Security Architecture): Inbound. All land on your ingress controller, over your own path (internal DNS, VPN, private load balancer). The host names are the chart’s defaults and each can be renamed (Domains, Ingress & DNS):

Prerequisites

Published requirements: requirements.json.
  • The executor fleet (where Claude Code, Codex and OpenCode turns run) adds to these numbers and needs a namespace that admits PodSecurity baseline, Landlock on its nodes (or global.fleet.landlock.enabled: false) and XFS with prjquota. No privileged container or node preparation. See Node prerequisites.
  • The LLM Gateway is always on, and agents’ model calls go through it. It adds its pods (0.5 vCPU / 1 GiB requested each, up to 1 vCPU / 4 GiB) and the llm-gw image. See LLM Gateway on a self-hosted install.
The fleet starts warm with one executor and needs a StorageClass that mounts XFS with prjquota for its 500 GiB per-executor volume: Without that class or driver, the chart renders the executor idle at replicas: 0, annotated xpander.ai/fleet-idle-reason, so an --atomic install never waits on a volume that cannot bind. Create the class and the next upgrade starts it. Sizing tiers: Sizing and requirements.

Install in three steps

1

Receive the license files and pull token by email

The xpander.ai team emails you:Details: Two-File Licensing.
2

Run the one-liner

From a terminal with kubectl access to the cluster:
A legacy v1 install passes --license-file ./license.json instead of the three flags.The wizard runs preflight checks, installs with Helm and seeds an admin login. It prompts for:
  • Registry
  • Chart source (OCI pull or local path)
  • Data tier
  • Admin email
  • Organization display name
  • StorageClass
  • Agent code execution (--code-execution / --no-code-execution). The runner embeds dockerd, so its pod is privileged, and a restricted PodSecurity policy refuses it
  • A base domain for the public hosts
  • LLM provider keys, for any hosted providers your egress policy allows
Image tags come from the release.If your registry has no xpander images yet, the wizard offers to mirror them right there with the pull token from step 1, and creates ECR repositories itself. On a sealed network, use Sealed / manual mirroring.
3

Sign in as the seeded admin

The first install creates your organization and a bootstrap admin in your in-cluster Supabase. The closing summary prints how to sign in: see Signing In.
Non-interactive (image tags resolve from the mirror unless pinned):
To review every command first, bash install.sh --edition airgap --manual prints a personalized runbook and runs nothing: see Advanced: review-first / manual install.

What happens when you run the installer

Steps 1–6 take a few minutes. Step 7 takes 10–20 minutes. Each phase prints a [N/8] name: ... line for scripts to grep (Automation & CI).
Every hook failure names the Job, prints its last 30 log lines, and ends with a FIX: line and a RERUN: line that resumes the install.
1

Resolve: load the latest release

One release manifest lists the version and SHA-256 digest of every xpander image, including llm-gw and tool-repo-seed. The installer passes those pins to Helm. To repeat a release or install offline, pass --release-manifest ./release-manifest.json. A missing or invalid manifest stops the run. How releases work.
2

Checklist: preconditions verified before anything changes

The registry answers /v2/ (401 counts as up), agent-controller and deployment-manager exist at their pinned tags, an RWO StorageClass exists, the image pins are in place, --org-name and --admin-email are set, and a chart source exists. Any failure stops here with nothing to clean up.
3

License: validated locally, applied as a Secret

Two xpander-license/v2 envelopes, one kind: install and one kind: activation, bound to --org (a legacy v1 file: envelope, expiry and org id). They become the xpander-license Secret. The Ed25519 signature is verified in the preflight hook and again at deployment-manager boot.
4

Chart: pulled over OCI

The xpander-airgap chart comes from the gated OCI registry, your own mirror, or a .tgz via --chart-path.
5

Values: secrets never touch the command line

Configuration goes to a temporary values file (mode 600, removed on exit), never to the helm command line, shell history or process list.
6

Install: helm, without --atomic on purpose

The first install runs without --atomic, because --wait would hold the seed hooks that the api and agent-controller pods are waiting on. The smoke hook is the readiness gate. Upgrades always run with --atomic, so a failed upgrade rolls back and the previous revision keeps serving.
7

Pipeline: the cluster comes up, hook by hook (10–20 min)

The installer watches the hook pipeline live:
8

Summary: URLs, password, logs

The closing box prints the derived URLs (with a base domain: https://chat.<domain>, https://api.<domain>, https://supabase.<domain>), the bootstrap admin password, and how to read it later:
The secret-scrubbed session log (mode 600) is under ~/.xpander/logs/, with latest.log pointing at the last run.

Releases

The release manifest supplies xpander image versions and digests to Helm. Helm still deploys. By default, the installer and mirror script use the latest release. A manifest names every xpander image: the services (including billing, openapi-spec-generator and llm-gw), the agent runtime images, and the artifacts (UI, migrations, edge functions, skills, catalog specs and tool-repo-seed). Logos ship in the UI image.
  • Latest release: omit --release-manifest on a connected machine.
  • Repeat a release: pass the same --release-manifest FILE to both scripts, so a new release between mirroring and installing changes nothing.
  • Invalid release: stops the run. It never falls back to latest or guesses tags.
  • Consistency: tag flags must match the manifest, and its pins override conflicting image values. Registry, infrastructure and secrets stay yours.
Third-party image versions come from the chart, so keep the matching chart package with each release. Keep mirror tags longer than the releases you may re-install. A “keep the last twenty images” rule spans only days, and a re-install then fails on an expired image. Keep hundreds of tags, or exempt pinned ones. If repositories must exist before a push, create one per release image, including agent-harness, openapi-spec-generator, the third-party data-tier and Supabase images, and charts/xpander-airgap. image_sync.sh creates missing ECR repositories itself. Tracking pre-release builds (staging only): set XPANDER_IMAGE_STREAM=dev for the installer and pass --stream dev to image_sync.sh. Otherwise an air-gapped install refuses development overrides. Per-artifact tag flags: Other flags: --manifest-out <file> writes the copied tags as a values file (pass it last with --values). --release-candidate selects a candidate. Credentials: image_sync.sh --source-user xpanderaihub --source-token <token> (and --dest-user / --dest-token), installer --xpander-registry-token <token>. --icon-pack-tag no longer exists, and image_sync.sh refuses unknown flags.
Installer 0.17.1 cannot parse the JSON release manifest, so --release-manifest alone fails. Until the fix ships, mirror with --manifest-out mirror-pins.yaml and pass --values mirror-pins.yaml as the last values file, alongside --release-manifest.
Publishing a release does not upgrade clusters. Your Helm, GitOps or upgrade schedule decides when.

Existing Helm and GitOps pipelines

Example pipeline: CI/CD & GitOps.

When the mirror is empty, the wizard offers to fill it

Choose Mirror them now, give the xpander pull credentials and any destination credentials, and let the copy finish. ECR repositories are created when your AWS credentials allow. Non-interactively: --mirror now|guide|skip (now needs the credentials). Sealed networks use the archive flow below.

Sealed / manual mirroring

Mirror with the current image_sync.sh: it includes llm-gw and tool-repo-seed, and stops on a release without the gateway image. It needs skopeo and Python 3. The Docker pull/tag/push fallback does not preserve release digests. On a connected workstation, download the manifest once and use that file for both the copy and the upgrade:
Pass registry credentials with the source/destination flags or registry auth. --dry-run prints the plan. Keep the JSON manifest: a single --tag or a legacy service-only manifest is not a complete release. Fully sealed network? Archive outside, carry the whole directory in, import inside:
Carry install.sh, image_sync.sh and their release_manifest.py with the bundle. Get the validator from the immutable release_manifest-<sha256>.py URL in the scripts and save it as release_manifest.py beside them. The scripts verify its checksum. Import validates the full image set and digests first. Complete an incomplete archive on the connected side. Legacy archives need the previous importer. The chart is an OCI artifact. Mirror it with the images (--chart-version <X.Y.Z> --chart-source registry-1.docker.io/xpanderaihub/xpander-airgap, landing at oci://<your-registry>/xpander/charts/xpander-airgap), or hand-carry the .tgz:
Hosted repository: mirror into it. Use it as --registry, and fill it with the wizard or image_sync.sh. --dest-user / --dest-token handle Basic auth:
Then install with --registry nexus.corp.example:5000/xpander.Pull-through proxy, no mirror step. Create a Docker proxy repository upstream of Docker Hub (https://registry-1.docker.io) with the xpander read token as its upstream credentials, and keep the org path in the registry value:
Images cache on first pull. Keep the xpanderaihub segment: the chart builds <registry>/<name>, so <proxy-repo>/xpanderaihub/agent-controller maps to xpanderaihub/agent-controller upstream. A fully sealed network cannot use a proxy: use the archive flow into a hosted repository.
bash install.sh --edition airgap --guide prints this runbook personalized with your registry and namespace (also printed when no cluster is reachable). --manual prints the whole install as commands and runs nothing.

Data tier: in-cluster or external

Choose carefully: moving from chart-managed StatefulSets to a managed service later is a migration. The wizard asks per store. Both default to in-cluster.
Chart-managed StatefulSets on your StorageClass:
  • PostgreSQL 16: history, memory and per-agent databases
  • Redis Stack (redis-stack-server): queues, cache and knowledge-base vectors. Not plain Redis: knowledge bases use RediSearch (FT.*)
  • MongoDB: catalog and monitoring
  • Supabase ships its own pinned PostgreSQL 15 (it needs extensions managed services lack)
No flags needed. --storage-class <name> picks a non-default class.

Local LLM inference

xpander speaks the OpenAI-compatible API, so any local runtime that exposes it plugs in: Size the inference pool separately: it is your infrastructure, not part of the chart. Hosted providers also work if your egress allows, with the same LLM provider keys. Two models are declared at install: the agents’ default and the gateway model that filters requests first (global.llm.gateway.{provider,model,apiBase}). Each install or upgrade converges the organization’s defaults to the chart’s declaration only when that declaration changed, so defaults an admin edited in the UI survive upgrades.
Amazon Bedrock without a key. In images built after 27 August 2026, chat, tasks and their summaries use the pods’ own AWS identity (EKS Pod Identity or IRSA): leave global.llm.bedrock.bearerTokenSecret empty and set global.llm.bedrock.region.
  • The pod role needs bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream, bedrock:Converse and bedrock:ConverseStream on * (the global.* profiles route across regions), bound to both default and agent-controller ServiceAccounts. No second role is assumed.
  • The account must be opted in to Bedrock data retention for those models in every region a global. profile spans. A sealed network needs VPC endpoints for bedrock-runtime, sts and eks-auth.
  • A Bedrock API key in bearerTokenSecret is the alternative.
  • The “IAM role” method under Platform > AI governance > Models applies to the Claude Code leg only. If such an entry exists and chat fails, remove it and restart agent-controller, agent-worker, agents, actions and functions.
A working keyless declaration, from a customer install on EKS:
Leave bearerTokenSecret and assumeRoleArn unset, put the Bedrock permissions on the role the pods run as (xpander-sa-role there), bind it to default and agent-controller, and upgrade. No separate Bedrock role is needed. Claude Code on Bedrock: the CLI calls the in-cluster LLM Gateway directly, and the gateway gets the Bedrock credential from the Agent Controller, the only place assume-role applies: global.llm.bedrock.assumeRoleArn (plus assumeRoleExternalId), or with no token and no role, the pod’s own credentials. global.llm.bedrock.baseUrl points it at your VPC endpoint. Fable- and Mythos-class models also need the per-region data-retention opt-in. See AI vendors. Summaries around agent work (thread titles, context compaction, evaluation judges) use the org default model, backed by the install’s other providers. None dials a public endpoint on a sealed install.

A worked vLLM setup on EKS Auto Mode

Checked end to end on an Air-Gapped install: vLLM serving a small instruct model on a T4, registered as a custom provider. The cluster side is yours. xpander needs only the address. GPU capacity. A NodePool limited to g4dn with a nvidia.com/gpu taint gets a T4 node in about two minutes, exposing nvidia.com/gpu: 1 with no device plugin. It needs nodeClassRef group: eks.amazonaws.com, kind: NodeClass, name: default. Keep weights on a PersistentVolumeClaim mounted as HF_HOME (pre-loaded on a sealed network). Five things that stop vLLM:
  1. A Service named vllm injects VLLM_PORT=tcp://... and vLLM refuses to start (VLLM_PORT ... appears to be a URI). Set enableServiceLinks: false or rename the Service.
  2. A T4 needs --dtype=half. vLLM 0.11 crash-loops on a T4. vllm/vllm-openai:v0.9.2 works.
  3. Pass the key as VLLM_API_KEY, not --api-key (which logs it). It guards only /v1. /health, /metrics and /tokenize answer without it, so keep port 8000 on a ClusterIP Service, never an ingress or load balancer.
  4. Start with --enable-auto-tool-choice --tool-call-parser=hermes, or Add provider marks the model as unable to run agents.
  5. Leave room above the composed prompt. --max-model-len=8192 failed with 400 This model's maximum context length is 8192 tokens. However, you requested 8298 tokens, shown as Unknown model error (controller log: Non-retryable model provider error: Unknown model error next to [prompt-budget] system_message_tok=...). --max-model-len=32768 ran. Custom models also log unknown model.id ... defaulting to 128,000 window.
The manifests, with account-specific values replaced by placeholders:

Register the endpoint in xpander

  1. Platform > AI governance > Models, Add provider.
  2. Name is free text, and ID (used in API calls) derives from it and is fixed. Protocol: OpenAI-compatible. Base URL: the address the pods reach, for example http://vllm.<namespace>.svc.cluster.local:8000/v1. In-cluster addresses work on Hybrid and Air-Gapped. xpander cloud accepts only a public https address.
  3. API key: No key (local or unauthenticated server), Use an existing key, or Create a new key with a Key name and the API key (the VLLM_API_KEY value).
  4. Test connection shows Reachable and the served models. A tools badge marks models that accept function calls. Add the model, with an optional Display name.
  5. Click Add provider.
  6. In the provider’s Keys and models…, tick the models the organization may use. Prices (USD per 1M tokens, via Edit model) track spend and are never charged. Who may use it… sets access. New models wait under New models unless Approve new models automatically is on. Agent readiness shows, from the last Test model, which harnesses work with each model.
Models page with the xpander.ai vllm provider panel open, showing Keys, LLM gateway, New models with Approve new models automatically, a Models list with muse-glimmer ticked and its prices

Keys and models on a custom provider: the key in use, New models waiting for approval, the models the organization may use with their prices. Shown with sample data and a different model than the one deployed above.

POST /v1/agents accepts only built-in providers in model_provider (a custom ID gets 422). Pick a custom provider per conversation in Harness and model. The header then reads, for example, OpenCode · Qwen2.5 1.5B (lab T4), the controller logs gateway turn start with model=<host>:<port>/<model id>, and vLLM logs POST /v1/chat/completions 200.

The executor fleet on Air-Gapped

Agent turns on Claude Code, Codex and OpenCode run on the executor fleet. Sizing: Sizing and requirements. On Air-Gapped, also set:

AI skill generation

Upload an OpenAPI spec (Connectors → Create connector) and the openapi-spec-generator service turns it into a full skill in the background: grouping, naming, descriptions, summaries and validation. You can delete generated skills or upload a new spec version over one. The air-gap license carries the entitlement (Custom connectors), and the image is in the standard mirrored set. The generator uses exactly one LLM provider, chosen at install: Bedrock if configured, else OpenAI if a key was given. It refuses runs without that provider’s credentials. With neither, it is not installed and skill creation is spec-only, with no enrichment (VITE_CONNECTOR_AI_ENRICHMENT is set for you). It writes specs to an object store bucket, set on both catalog and openapi-spec-generator, with versioning enabled (it lists object versions). Without the bucket, upload fails.
On Bedrock, the generator follows global.llm.bedrock.region, falling back to us-west-2. Its default models are US cross-region profiles, so that region must be in the US, and IAM and PrivateLink must cover Bedrock there.

Custom actions and code execution

Custom actions and code run in short-lived jobs from the actions service. To let them reach internal systems, give them the cluster’s resolver:
The job image is xpander’s sandbox image from your mirror. api.env.SANDBOX_IMAGE overrides it for a development stream.

The license

New installs use two sealed files (install.json + activation.json, xpander-license/v2): see Two-File Licensing for caps, expiry and renewal. The installer validates both and creates the xpander-license Secret:
The preflight hook verifies the Ed25519 signature against global.license.publicKey, and deployment-manager refuses to start on a bad one.
  • Your own --values file displaces the installer’s license block: also pass --set global.license.installKey=install.json --set global.license.activationKey=activation.json --set global.license.publicKey=<license public key>.
  • Renewing replaces only activation.json and redeploys. See Renewing: re-activate.
  • Expired activation: the installer warns and preflight refuses a fresh install. A running install degrades in stages. GET /v1/license/status returns valid, expiring, grace or expired. Alert on anything past expiring.
  • Single-tenant: one license, one organization.
  • Legacy single file (xpander-license/v1): --license-file ./license.json creates a Secret with one license.json key, wired through global.license.key. Renew by replacing that file and redeploying.

Domains, ingress & DNS

One --domain (for example xpander.acme.internal) names every public host: A reference customer renames most of them: app. (UI), auth. (Supabase), api., chat. (agent-controller), hooks. (webhook), links. (short links, the functions Service), actions., client-auth., and dm.internal. for deployment-manager on a second, internal ingress class (deployment-manager.ingress.className), reached over a backchannel. Set each with <name>.ingress.host (the UI also with --ui-host), and move the matching knobs in the same change: Setting the domain publishes these Ingresses. <name>.ingress.enabled: false keeps one private, and global.ingress.enabled: false suppresses all. The domain also sets the UI’s runtime origins: without it, or ui.env overrides, browsers on a sealed network get unreachable origins.
  • --ingress-scheme https|http (default https) feeds every derived URL. Use http only without certificates.
  • No ingress controller is assumed: set <name>.ingress.className / annotations, or global.ingress.className / global.ingress.annotations.
TLS at your controller: the Ingress tls: block, per component (<name>.ingress.tls) or via global.ingress.tls. A wildcard *.xpander.acme.internal covers the derived set. dm.internal.<domain> needs its own wildcard:
TLS at a cloud load balancer (ALB, Azure Application Gateway, Google): a managed certificate covers every host, and the Ingresses carry annotations and no tls:. A wildcard covers one level only: *.xpander.acme.example does not cover *.staging.xpander.acme.example. On EKS Auto Mode, put the group in an IngressClassParams (Auto Mode ignores alb.ingress.kubernetes.io/group.name). The load balancer appears after the first Ingress, so DNS aliases are a second pass.

Making the hosts resolve

A wildcard in the zone your users resolve:
Or one record per host when wildcards are not allowed.
Sealed networks: resolution must happen where your users are (internal zone or VPN DNS). The domain never needs to exist publicly.

Signing in

The summary box prints the URLs and the bootstrap admin password. helm get notes xpander -n xpander repeats the commands.
  1. Bootstrap password (default): open https://chat.<domain>/login-password with your --admin-email. Read the password again with:
  2. Email OTP without SMTP: codes are not sent. Get one with scripts/get_login_code.sh <email> <namespace>. With global.smtp.* set, codes arrive by email. On Amazon SES in the sandbox, invitations reach only verified identities, and the SMTP IAM policy must list each verified recipient identity ARN: request production access first (Email). Agent mailbox replies use the same relay: INBOUND_EMAIL_REPLY_DOMAIN sets the From domain, and the SMTP identity must be allowed to send as the agents’ mailbox subdomain.
  3. SSO (OIDC): configured in xpander Chat under Platform > Organization > Access and served by client-auth. See SSO with your identity provider. Google and GitHub buttons stay GoTrue providers (supabase.auth.extraEnv).
No domain yet? Port-forward to the UI:
Access tokens live one day (supabase.db.jwtExpiry, default 86400), renewed through the rotating refresh token. The one value feeds GoTrue, PostgREST and the database. Older tokens keep their expiry until refresh, and the migrations hook re-applies it on every upgrade, so installs that started on the one-hour default converge.

Upgrading an Air-Gapped install

Slack handle management compatibility

For releases with administrator-authorized Slack agent handles, mirror the matching Supabase migrations and bots images before upgrading the chart. The migrations image must include 20260910231402 and 20260911192635, and the chart’s post-migration check requires get_slack_handle_authorization, save_slack_handle_authorization and complete_slack_handle_authorization. Never upgrade the chart alone before those images are in your mirror. Add usergroups:write to your Slack app’s User Token Scopes, then complete administrator consent in Platform > Agents > Channels, on the Slack row. The token is encrypted in the local Supabase Vault. Slack needs explicitly allowed connectivity, and this upgrade does not turn it on.

Release v2026.10.22 (chart xpander-airgap 0.17.2)

Carries the changes of Hybrid chart 0.22.5 (Hybrid: Upgrading to the current chart). Before you upgrade:
  • The LLM Gateway is always on and llm-gw.enabled is ignored. If you had it off, mirror llm-gw, give every provider your agents use a key, and open its network paths (LLM Gateway on a self-hosted install).
  • In-cluster Postgres moves to the pgvector build of PostgreSQL 16.15. Mirror pgvector/pgvector, and snapshot the Postgres volume or set postgres.upgradeGuard.backup.pvcName. Postgres restarts once. A pinned postgres.image must stay on a pgvector build of the same major version.
  • Per-agent MCP URLs are gone. Use https://api.<your domain>/mcp/. There is no per-person library endpoint on air-gap, and Omni over MCP needs global.mcp.publicUrl.
  • BRAIN_INGEST_API_KEY is added to the generated secret automatically.
Mirror and upgrade (replace <namespace>, <your-registry> and <xpander-token>, Helm 3.14+):
Then run the verification guide.

Release v2026.10.15 (chart xpander-airgap 0.16.2)

Carries the changes of Hybrid chart 0.22.0 (Hybrid: Upgrading to the current chart):
  • Mirror with the current image_sync.sh (adds llm-gw and tool-repo-seed, drops --icon-pack-tag).
  • The LLM Gateway is on by default. Its generated keys are kept on every upgrade. Never rotate the content key. Every provider your agents use needs a key, or calls are refused naming the missing key. Check kubectl -n <namespace> get pods -l app=llm-gw: two llm-gw pods (one on a single-node install.sh cluster), 1/1 Running. See LLM Gateway on a self-hosted install.
  • Key names and endpoints: env-var key names matching the install’s own credentials read as no key, and metadata-address endpoints are refused (details).
  • Helm 3.14+ for --reset-then-reuse-values. Migrations run automatically.

Upgrade procedure

Check the latest release. Two steps, in this order:
1

Mirror the new bundle FIRST

Pinned digests mean upgrading before mirroring leaves pods in ImagePullBackOff. Re-run image_sync.sh (curl -fsSLO https://charts.xpander.ai/image_sync.sh) for the target release as in Sealed / manual mirroring, and carry the new chart .tgz if needed.
2

Run the upgrade

It reuses the installed values (helm get values), loads the release pins over them, and stops if the manifest is invalid. It runs --atomic. Migrations stay forward-only. Pipelines should use the declarative re-install instead.

Declarative re-install for CI

A pipeline should inherit nothing from the release it upgrades. Run bash install.sh --edition airgap ... on every deploy with all values files, the release manifest and mirror pins, the license flags and --org, as on day one. Against a deployed release the installer adds --atomic itself. (install.sh upgrade once let a stored digest ride along and ran a stale image on staging for four days.)
  • --helm-timeout 20m (default 30m). The --atomic rollback waits the same time again, so budget twice the value plus hooks within your job’s credential lifetime. A one-hour OIDC session cannot host a 30-minute timeout.
  • Heal first. The installer refuses a release in pending-install, pending-upgrade or failed. Check helm history xpander -n xpander, then helm rollback xpander <last deployed revision> -n xpander, or helm uninstall xpander -n xpander when none deployed (xpander-generated-keys survives). Delete Failed pods and failed hook Jobs first.
  • Pin the environment id. After the first install, pass --env <existing default environment id>: the default location’s runtime environment cannot move to a new id. Read it first:
  • Wait for Helm’s verdict. The stage watcher exits on the first failed hook pod, even an evicted one whose Job then succeeds. If install.sh exits non-zero while helm upgrade --install xpander still runs, do not kill Helm (it strands the release in pending-upgrade). Wait and read the release status.
  • global.upgradeStrategy: replace on clusters that provision nodes on demand, so a surge upgrade does not trigger provisioning waves that outlive the timeout.
The fleet and --atomic. A running harness turn holds the roll: on SIGTERM an executor drains for up to global.fleet.executor.drainSeconds (3600 s). The installer’s timeout covers it. On a hand-driven helm upgrade, set --timeout to at least the drain window, or upgrade when quiet. A turn still running at the deadline stops with “The executor is restarting; send the message again to continue where it left off.” and resumes when the user sends again. An idle executor (zero replicas, xpander.ai/fleet-idle-reason) is skipped and never stalls an upgrade. What survives what:
  • Generated keys survive every upgrade and rollback (lookup-preserve, and the Secret is Job-created, never Helm-rendered).
  • helm rollback xpander <N> restores any prior revision, except the database schema: migrations are forward-only, so a schema rollback means restore-from-backup.
  • The one-day token lifetime (supabase.db.jwtExpiry) is re-applied by the migrations hook on every upgrade.
Avoid --reuse-values on hand-driven air-gap upgrades. It replays the old computed values, keeping changed defaults (from v2026.10.15 the LLM Gateway would stay off) and resurrecting removed settings, such as a stale edge-runtime image pin that crash-loops and fails every --atomic upgrade. Re-supply your values files, or clear it once with --set supabase.edgeRuntime.image.repository=null --set supabase.edgeRuntime.image.tag=null.

Automation & CI

  • Every prompt has a flag. --non-interactive reads nothing from the terminal, exits 2 with the usage and a rerun line when a value is missing, and implies --yes and --on-probe-failure abort.
  • Stable progress lines such as [3/8] license: ok. --quiet prints phases and the outcome only. Color only on a terminal.
  • status is a read-only health report (edition, chart, release, hooks, readiness, URLs). --json emits one object.
Exit codes: Air-gap, end to end, no terminal (release already mirrored):
If the mirror may be empty, add --mirror now --xpander-registry-token <token>. Without a --mirror answer, an empty mirror exits 2. Poll until healthy:

Reference values file

A complete production values file. Hosts, buckets and endpoints are placeholders from your infrastructure outputs. Pass passwords with --set, and the mirror manifest last for image pins.
Never repeat the installer’s own keys (orgSeed, generated passwords, image pins): extra values files merge last and override them.

CI/CD & GitOps

Fetch the release manifest once, mirror it with CI-stored credentials, re-run the install with that file, your values and the license flags (declarative re-install), then gate on status. The agent needs cluster access, Helm 3.13+, skopeo and Python 3.
The install step fails on 1 (upgrade failed, already rolled back by --atomic), 2 (usage) and 4 (preflight). status fails on 1 (degraded, with reasons) and 3 (not installed). status --json emits one object if you prefer to parse.
Mirror the chart with the images and keep configuration and generated release values in Git. A newer chart alone does not fetch the release manifest. Generate values with your install arguments plus --release-manifest ./release-manifest.json --values-out xpander/values.yaml. It writes xpander/values.yaml and xpander/values.yaml.release.json (Helm values, not the manifest). Keep the manifest for your record and pass both files, release values last:
For Argo CD:
  • Values: regenerate per release, review with the chart version, and add no image overrides after the release values. Direct Helm does not run the installer’s checks. Strip secrets (LLM keys, DSNs) into your secret manager before committing.
  • Bootstrap secrets: create xpander-license (and any pull secret) once out-of-band (The License), or with SealedSecrets / External Secrets Operator.
  • Hooks: Argo CD maps the chart’s hook pipeline onto sync phases and waves. The chart is not certified under Argo CD: verify the hook ordering on your Argo version. install.sh is the certified path.

Skill credentials on Air-Gapped

Built-in skills that call a third-party service need a credential, or they fail with a 401:
  • RapidAPI skills (for example Google News, Company Insights and Social Media): set global.network.toolKeys.RAPID_API_KEY, or calls return 401 Invalid API key.
  • Skills with their own sign-in (for example BigQuery and Figma): create the connection with valid credentials.
  • AWS skills (Redshift, S3, Athena, EKS): the IAM role and its permissions on AWS Operator Setup.
  • Public skills need internet egress, and do not work with global.network.internetEgress: false.

Backups

The chart creates no backup job. Back up these volumes yourself, for example with AWS Backup or EBS snapshots: External PostgreSQL or Redis use the managed service’s backups. Migrations are forward-only, so a schema rollback needs a restore.

Monitoring

No metrics stack or ServiceMonitor ships. The xpander-smoke Job logs smoke passed: every required component serves when all components answer, and bash install.sh status gives a read-only verdict. Scrape the services with your own stack for metrics.

Troubleshooting

Every failure prints FIX: (the repair) and RERUN: (your non-interactive one-liner, secrets as placeholders). The installer is idempotent: fix, paste the rerun line, and it resumes. The checklist stops most problems before anything changes. Scripts read the exit code (Automation & CI), and bash install.sh status gives a read-only verdict.

Mirroring and registries

ECR refuses pushes to repositories that do not exist. The current image_sync.sh creates them (with AWS CLI credentials). Refresh your copy:
On an older copy, pre-create the repository and re-run (copied images are skipped):
authentication required even though docker login works: skopeo cannot read Docker Desktop’s credential store. The current image_sync.sh logs in to ECR itself (aws ecr get-login-password | skopeo login --username AWS --password-stdin). For other registries, pass credentials:
401 from auth.docker.io (incorrect username or password): the credential is the organization username xpanderaihub plus the token from your email, not a personal username. Re-run with --xpander-registry-user xpanderaihub --xpander-registry-token <token>. Still refused? Ask xpander to re-issue the token with repository-read and image-pull access.
Pod events show manifest unknown, or the checklist reports:
The mirror lacks that repository at the pinned tag. Tags differ per image, so one stale repo is enough. Re-run the mirroring (in-wizard or image_sync.sh), then check the exact coordinate:

Cluster and network

An image reference still points at docker.io instead of your mirror. Install with --registry <your-mirror>/<path>, and make sure the nodes reach the mirror (on an ECR-only sealed VPC: ecr.api + ecr.dkr endpoints, the S3 gateway endpoint, ECR read on the node role). Find the reference:
(On a mixed cluster: exec format error.) xpander images are amd64-only, so ARM/Graviton (t4g, m7g, c7g, …) cannot run them. Add an amd64 node group. Only for genuinely arm64 images: --set 'preflight.allowedArchitectures[0]=arm64'.
The ingress Service’s events show:
A sealed VPC has no public subnets and the private ones lack ELB role tags. Tag them and annotate the Service:
Or skip the load balancer: use the VPN, or kubectl -n xpander port-forward svc/ui 8080:80.
The probe ran before the namespace existed (fixed in current installers). Refresh (curl -fsSL https://charts.xpander.ai/install.sh) or create the namespace and re-run:
Unbound PVCs (kubectl -n xpander describe pvc) mean no usable RWO StorageClass or too little capacity for the footprint. Check kubectl get storageclass, pass --storage-class <name>, and look for FailedScheduling in kubectl -n xpander get events --sort-by=.lastTimestamp.
The browser console shows:
Charts before 0.6.0 did not derive the UI’s origins from the domain. Upgrade with bash install.sh upgrade, or set ui.env (VITE_SUPABASE_URL, VITE_CHAT_BACKEND_URL, …) yourself.
chat.<domain> opens but sign-in or API calls fail: only one of the published hosts has a record. Add a wildcard *.<domain>, or records for chat, api, agent-controller, supabase, webhook, client-auth and actions (or your <name>.ingress.host values). Check what is published:

Install and upgrade

The installer refuses a stuck release, and install.sh upgrade needs deployed. Recover first:
Then paste your RERUN: line. pending-install (an interrupted first install) is fixed with helm uninstall. xpander-generated-keys survives it, so the reinstall keeps the same keys.
Load a complete latest release, mirror its images, and retry with that manifest. For direct Helm, pass both generated values files, release values last. Never guess a shared image tag.
The installer checks the two xpander-license/v2 files (or the legacy v1 file) and the --org binding. Preflight repeats it with the Ed25519 signature, and deployment-manager repeats it at boot. GET /v1/license/status answers valid, expiring, grace or expired.Expired: the installer warns License expired on <date> - preflight will refuse the install. Replace only activation.json in the Secret with the renewed file and redeploy:
Then run install.sh upgrade or your pipeline’s re-install (the Secret checksum rolls the services). Keep the --set global.license.installKey/activationKey/publicKey flags if you pass your own --values (The License). A legacy v1 install swaps its license.json the same way.
Usually the agent holds a connection to an API connector that never finished (status faulty, no spec). The agents pod log shows a pydantic error on ConnectedConnector.open_api_spec_id. Recover from the in-cluster Supabase database:
Reload the agent in every open tab before saving it again: a stale tab writes the dead connection back.
The installer printed the Job’s last 30 lines. Dig deeper:
org-seed needs --org-name and --admin-email whenever Supabase is on. A hand-driven helm upgrade without them fails at render time.
A username with password: "" beside it overrides the generated password with an empty string. Remove the empty password: key: an absent key is preserved, "" wins.
On releases before the single-provider generator, a missing OpenAI key made the dialog show “The AI enrichment pipeline reported a failure” although the skill finished seconds later. Check the skills list before retrying. Upgrading fixes it.
You upgraded before mirroring (typically manifest unknown). Mirror the target release and re-run the upgrade. --atomic already rolled back the failed attempt.
--reuse-values resurrected a stale edge-runtime image pin. Upgrade with your real values files (as install.sh upgrade does), or clear it once:
Better: pin the mirrored edge-functions build (--edge-functions-tag).
Hybrid and Air-Gapped cannot be upgraded into each other. Use another --release / --namespace, or helm uninstall xpander -n xpander first.
Piped into bash, the run is non-interactive and needs --org, --install-license-file and --activation-license-file (or --license-file for v1). It exits with code 2 and a rerun line. For the wizard, download the script and run bash install.sh --edition airgap.

Next steps

Review the security architecture

Zero egress, signed license, local sign-in.

Review the install before you run it

--manual, --values-out and --dry-run.
The chart README (in the .tgz) covers bring-your-own ingress, private CA and proxy, SMTP, internet posture, builtin-skills seeding and Bedrock region pinning.