Architecture
Hybrid follows the control plane / data plane separation pattern — the same model used by AWS EKS, Datadog, and other infrastructure platforms:- Control plane (xpander Cloud) — manages environment metadata, agent registration, connector definitions, and event logging. AI engineers use the Agent Workbench to define agents, configure connectors, and validate tasks. The control plane never touches runtime data.
- Data plane (your VPC) — runs all task execution, agent memory, connector credentials, model API keys, and user-facing services. The user’s browser connects directly to the data plane. Chat threads, activity views, and task results are served from the cluster — nothing passes through the cloud.
What Gets Deployed
How It Works
Clients — Slack, the web UI, MCP clients, the SDK and API — connect directly to the cluster in your VPC. When usingapp.xpander.ai, the cloud UI handles authentication and tells the client which self-hosted cluster to connect to; all data then flows client-to-cluster. Inside the cluster, the Agent Controller schedules tasks and Agent Workers execute them — calling LLMs, persisting messages in PostgreSQL, and reaching external tools exclusively through the AI Gateway.
The only connection to xpander Cloud is the deployment manager sync: the Agent Controller authenticates with organization_id + environment_id + api_key, reports heartbeats, and pulls agent, connector, and MCP configuration.
What Talks to What
The egress surface — the answers a security review needs. Inbound from xpander cloud: none; connections are outbound-only.Data Residency
Requirements & Sizing
Minimum footprint (the installer shows the same numbers, published at charts.xpander.ai/requirements.json):Example Estimates
Agents are autonomous — tasks come from schedules, webhooks, the SDK and API, and other agents, not only chat (in xpander cloud production, only ~5% of executions are interactive chat). Size for the number of tasks running concurrently at peak, whatever triggers them.Sizing Calculator
How the estimate works
How the estimate works
- Concurrent tasks is the primary driver. A task is any agent execution, from any source: chat, API/SDK calls, schedules, webhooks, MCP clients, Slack, or another agent. If you don’t know your peak concurrency yet, assume one concurrent task per agent - that covers most production organizations we measured. In practice most teams peak at just a few concurrent tasks (typically under 5), and even the busiest organization peaks around 30.
- Agent Workers = concurrent tasks ÷ 4 (each worker replica processes 4 tasks by default, configurable with
MAX_CONCURRENT_EXECUTIONS). Each replica requests 2 vCPU / 2.5 GiB. - Agent Controllers: 1 replica up to ~8 concurrent tasks, 2 up to ~20, 3 beyond. Each requests 1 vCPU / 2 GiB.
- Active workspaces = concurrent tasks × workspace share, capped at the agent count. Budget ~0.5 vCPU / 1 GiB per active workspace (measured typical load; each can burst to its 2 vCPU / 3 GiB limit).
- Fixed base (~2 vCPU / 6 GiB): AI Gateway, MCP, Chat, Code Runner, AWS Operator, API, Redis, PostgreSQL, container registry, metrics-server at one replica each.
- Cluster capacity = requests × 1.5 for burst, DaemonSets, and safe node drains. Storage = 48 GiB of chart volumes + 5 GiB per agent (each agent that uses a workspace keeps a persistent volume) + image cache per node.
Per-service requirements
Per-service requirements
Agent workspaces
Agent workspaces
Data services, storage, and the production checklist
Data services, storage, and the production checklist
- Node types: x86_64 only. General-purpose instances with a 1:4 vCPU:GiB ratio (
m5,m6i) fit the workload profile;t3burstable instances are fine for evaluation, not for production. - Raise the low defaults: API memory (table above) before real load.
- Enable autoscaling for Agent Worker once concurrency grows beyond a couple of replicas.
- Keep headroom: pod requests should stay under ~65% of cluster capacity so node upgrades and bursts don’t evict workloads.
- Storage class: SSD-backed volumes for Redis and PostgreSQL.
Install
1. Get Your One-Liner from the Console
- Go to app.xpander.ai/admin_settings#locations
- Click “Add Location”
- Choose the Hybrid edition. The dialog shows the minimum footprint for the edition you picked.
- Optionally set a URL (your server’s FQDN, e.g.
xpander.my-company.com) and the hostname style (dash-based subdomains if your DNS can’t do wildcards) - Copy the generated one-liner
--org, --env, and --dm-api-key (plus --domain when you set a URL). That is how the cluster authenticates to the xpander control plane:
2. Run the Installer
Run it from a terminal withkubectl access to the target cluster. The command downloads the installer from charts.xpander.ai and runs an interactive wizard — preflight checks, configuration, Helm install, then the connection back to xpander. Run bare (curl -fsSL https://charts.xpander.ai/install.sh | bash), it is the zero-knowledge entry point: the first question is which edition you are installing.
The wizard walks through, in order:
Edition & requirements
--edition wasn’t given, it asks. It then prints the minimum footprint for that edition (the same numbers as charts.xpander.ai/requirements.json) and asks you to confirm your cluster meets it — stopping cleanly if not.Identity
Ingress domain (optional)
agent-controller.xpander.<DOMAIN>, needs wildcard DNS) or dash style (agent-controller-xpander.<DOMAIN>, individual A records).Control plane reachability
https://deployment-manager.xpander.ai), AWS PrivateLink (https://deployment-manager-privatelink.xpander.ai), or Staging. The installer auto-detects staging/local environments (a .stg. or local domain, or a kind/k3d/minikube/docker-desktop kube context) and leads with that option; any of --staging, --privatelink, or --dm-url overrides detection.LLM providers (all optional)
AWS_BEARER_TOKEN_BEDROCK), or keyless IAM role / EKS pod identity. See the caveat below before choosing IAM.Namespace, context confirmation, summary
xpander, created if missing), an explicit confirmation of the kubectl context it is about to install into, and a masked configuration summary before anything changes.Install Phases
The Hybrid install runs six phases, each announced as a stable[N/6] name: ... progress line (the same shape scripts can grep — see Automation & CI):
FIX: line naming the action that repairs it and a RERUN: line with your personalized non-interactive one-liner (secrets typed at prompts are placeholdered, never echoed). Fix the condition and paste the rerun line — the installer is idempotent.
Amazon Bedrock: key vs IAM
--bedrock-auth key (default when --bedrock-key is given) stores the bearer token; --bedrock-auth iam is keyless and reads the pod’s own AWS credential chain.
3. Verify the Connection
When the install completes, a summary box prints the release, control-plane URL, ingress state, and configured providers. Your cluster registers with the control plane and the xpander console flips to “Connection established” in a minute or two.Running, 1/1-ready pod per application service (agent-controller, agent-worker, ai-gateway, api, aws-operator, chat, code-runner, mcp) plus the postgres and redis StatefulSets — and, at defaults, docker-registry and metrics-server.
Test locally without ingress:
PrivateLink
Your cluster must reach the xpander deployment manager. Two options:--privatelink (or pick AWS PrivateLink at the wizard’s reachability question). --privatelink, --staging, and --dm-url are mutually exclusive — pass exactly one, or none for the public endpoint.
How PrivateLink Works
AWS PrivateLink is a two-sided connection. xpander exposes the deployment manager as a VPC Endpoint Service (provider). You create a VPC Endpoint (consumer) in your VPC, which provisions a private ENI with a10.x.x.x IP address. Your pods connect to this private IP — traffic goes over the AWS backbone, never the internet.
Set It Up
Create the VPC endpoint
VpcEndpointId, Groups[0].GroupId (the security group), DnsEntries[0].DnsName, and DnsEntries[0].HostedZoneId.Allow HTTPS on the endpoint security group
Wait for the endpoint to become available
pending to available in 1-3 minutes (auto-accepted):Create private DNS
deployment-manager-privatelink.xpander.ai resolves to the endpoint ENI inside your VPC:HostedZone.Id from the output, then create the alias record:Verify connectivity
000? See Troubleshooting below.External Access & DNS
The xpander services communicate with each other internally via Kubernetes Services — no ingress is needed for that. You only need external access so that users can reach the chat UI, API, and other endpoints from their browser or client.--domain (or set ingress.enabled=true in Helm values) and skip to DNS records.Option A: VPN / Corporate Network Access
If your organization has a VPN that routes into the VPC, users can access xpander services directly — no ingress controller, NLB, ACM certificate, or public DNS needed. Point internal DNS records to the Kubernetes services:Option B: Public Access via Ingress + NLB
For public or internet-facing access, set up an ACM certificate, nginx ingress controller, and NLB.SSL Certificate
Request an ACM certificate for your domain.<DOMAIN> / *.<DOMAIN> (shared) and one for *.chat.<DOMAIN>:
Ingress Controller
Install the nginx ingress controller with AWS NLB and ACM SSL termination:DNS Records
Create wildcard CNAME records pointing*.<DOMAIN> and *.chat.<DOMAIN> to the NLB hostname. Both are required — the chat UI generates per-thread subdomains under chat.<DOMAIN>.
Managing LLM API Keys
Usehelm upgrade with --reuse-values to add or change API keys without affecting existing configuration:
kubectl get pods -n xpander.
Secret not updating after helm upgrade?
Secret not updating after helm upgrade?
xpander-static secret has a Helm resource keep policy — helm upgrade may not update it on subsequent installs. If your API key isn’t being picked up after a helm upgrade, set it directly in the secret:Using Kubernetes Secrets (Recommended for Production)
Using the SDK with Self-Hosted
Configure the xpander SDK to point to your Agent Controller endpoint.Using with Agno Framework
base_url points to the Agent Controller endpoint (e.g., https://agent-controller.{your-domain}), not the root domain.For more SDK examples, see the Self-Hosted SDK Configuration.Upgrading
Upgrades work like the Datadog agent flow: watch the xpander releases feed (Releases page / Atom feed), then run theupgrade subcommand:
install.sh upgrade:
- finds the release (default
xpanderin namespacexpander; override with--release/--namespace), - infers its edition from the installed chart — you never pass
--editionto an upgrade, - reuses the values the release was installed with (
helm get values), plus anything extra you pass (--set,--values), - shows current → target version with a link to the release notes, and asks before changing anything,
- always runs with
--atomic: a failed upgrade (including a failed hook) rolls back automatically and the previous revision keeps serving.
Installer Flags Reference
Automation & CI
The installer is built to be driven by scripts, CI pipelines, and coding agents:- Every prompt has a flag. With
--non-interactivenothing is read from the terminal, even when one exists: missing required values exit 2 with the usage and a completed rerun line rather than blocking. It implies--yes. - Stable progress output. Progress is one line per transition in a grep-friendly shape —
[3/6] chart-source: ok— and--quietreduces output to phase lines and the outcome only. Color is emitted only when stdout is a terminal. statusis the poll target. A read-only health report: edition, chart, release status, hook jobs, workload readiness, and URLs.--jsonemits one JSON object.
FIX:/RERUN: lines tell you what to repair — see Troubleshooting.
Advanced: Review-First / Manual Install
For teams that review before they run — everything the installer does can be inspected first, reproduced by hand, or both.Read the installer before running it
- The versioned URL is immutable —
install-v<version>.shis published once per release and never rewritten in place, so pin it in runbooks and CI (bash install.sh --versionprints the version you’re holding). - The script is shellcheck-clean, enforced by CI on every push.
- Secrets never touch the helm command line: they are written to a temporary values file (mode 600, removed on exit), so they don’t land in shell history or process listings.
- Nothing runs against a cluster before the kube-context confirmation — the installer names the context and cluster it is about to install into and waits for your yes.
Do it yourself
Manual Helm install (no installer at all)
Manual Helm install (no installer at all)
helm upgrade --install; everything it does can be done by hand. The cleanest middle path is --values-out:https://deployment-manager.xpander.ai, or https://deployment-manager-privatelink.xpander.ai with PrivateLink — and install:secrets.static.* values — the full list is in the key mapping table. Common optional parameters:--set flags:Troubleshooting
Every installer failure prints two lines:FIX: — the action that repairs the condition — and RERUN: — your personalized non-interactive one-liner (secrets typed at prompts are shown as placeholders). The installer is idempotent: fix the condition, paste the rerun line, and it picks up where the failure left off. A secret-scrubbed session log is kept under ~/.xpander/logs/ (latest.log points at the most recent run). For scripted runs, the exit code names the failure class (Automation & CI), and bash install.sh status gives a read-only health verdict.
Installer
Release stuck in pending-install / pending-upgrade / failed
Release stuck in pending-install / pending-upgrade / failed
install.sh upgrade requires the release to be in deployed status — operating on a stuck release (pending-install, pending-upgrade, pending-rollback, failed, uninstalling) would strand it. Recover first:RERUN: line. A pending-install status usually means a first install was interrupted (Ctrl-C, lost connection) or timed out — helm uninstall is the right recovery for it.Release already runs the other edition's chart
Release already runs the other edition's chart
--release / --namespace), or remove the existing release first (helm uninstall xpander -n xpander).Piped run exits with 'these required flags are missing'
Piped run exits with 'these required flags are missing'
bash leaves no stdin to prompt on, so a piped run is non-interactive (the same contract as --non-interactive): Hybrid requires --org, --env, and --dm-api-key. Missing flags exit with code 2 and a completed rerun line. Copy the personalized one-liner from the xpander console — it carries the identity flags — or download the script and run bash install.sh for the interactive wizard.Cluster and Pods
Pods stuck in Init
Pods stuck in Init
agent-controller to be ready. Check its logs:Pods stuck in Pending
Pods stuck in Pending
AmazonEBSCSIDriverPolicy attached.exec format error
exec format error
t3, m5, c5, etc.).Insufficient CPU
Insufficient CPU
agent-worker pod requests 2 CPU by default. Options:- Add more nodes or use larger instances
- For non-production environments only:
Health checks failing
Health checks failing
PrivateLink
PrivateLink InvalidServiceName (cross-region)
PrivateLink InvalidServiceName (cross-region)
us-west-2, you must include --service-region us-west-2:--service-region, AWS looks for the service in your local region and fails with InvalidServiceName.PrivateLink connection timeout (HTTP 000)
PrivateLink connection timeout (HTTP 000)
Ingress and Networking
Ingress not accessible
Ingress not accessible
Load Balancer not provisioning
Load Balancer not provisioning
<pending>. Check its events (kubectl -n ingress-nginx describe svc ingress-nginx-controller):could not find any suitable subnets for creating the ELB— the VPC’s subnets are missing the ELB role tags. Tag public subnetskubernetes.io/role/elb=1for an internet-facing NLB; for an internal one, tag the private subnetskubernetes.io/role/internal-elb=1and annotate the Serviceservice.beta.kubernetes.io/aws-load-balancer-internal: "true".- On EKS Auto Mode, ensure the cluster role trust policy includes
sts:TagSession:
SSL certificate errors on chat URLs
SSL certificate errors on chat URLs
*.chat.<DOMAIN> as a subject alternative name. The chat UI generates per-thread subdomains (e.g., moccasin-prawn.chat.<DOMAIN>) that are not covered by *.<DOMAIN>.Request a new certificate with:API Keys
API key not being picked up after helm upgrade
API key not being picked up after helm upgrade
xpander-static secret has a Helm resource keep policy. Set the key directly:LLM calls fail with a missing API key
LLM calls fail with a missing API key
--set secrets.static.<key>=...), or check that your secret is correctly mounted through envFromSecretKeys — see Managing LLM API Keys.
