> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hybrid: external access and DNS

> Let users reach the Hybrid chat UI, API and other endpoints, over a VPN or through ingress, an NLB and 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.

> [!NOTE]
>
> **Already have an ingress controller?** Give the installer a `--domain` (or set `ingress.enabled=true` in Helm values) and skip to [DNS records](#dns-records).

## Option A: VPN or 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:

```
chat.<DOMAIN>              → xpander-chat.xpander.svc.cluster.local
agent-controller.<DOMAIN>  → xpander-agent-controller.xpander.svc.cluster.local
ai-gateway.<DOMAIN>        → xpander-ai-gateway.xpander.svc.cluster.local
```

When installing, skip the domain prompt (ingress stays disabled, the default).

## Option B: Public access via ingress + NLB

For public or internet-facing access, set up an ACM certificate, an nginx ingress controller, and an NLB, using the three steps below: [SSL Certificate](#ssl-certificate), [Ingress Controller](#ingress-controller), and [DNS Records](#dns-records).

### SSL certificate

Request an ACM certificate for your domain.

> [!WARNING]
>
> The certificate **must** include a wildcard for `*.chat.<DOMAIN>` because the xpander chat UI generates per-thread subdomains (e.g., `moccasin-prawn.chat.<DOMAIN>`). Standard wildcards only match one level, so `*.<DOMAIN>` does not cover these. If you omit `*.chat.<DOMAIN>`, the chat UI will show SSL certificate errors for thread URLs.

```bash theme={"dark"}
aws acm request-certificate \
  --domain-name "<DOMAIN>" \
  --subject-alternative-names "*.<DOMAIN>" "*.chat.<DOMAIN>" \
  --validation-method DNS \
  --region <REGION> --profile <PROFILE>
```

Add the DNS validation CNAME records to your Route 53 hosted zone. There will be two unique validation records, one for `<DOMAIN>` / `*.<DOMAIN>` (shared) and one for `*.chat.<DOMAIN>`:

```bash theme={"dark"}
aws acm describe-certificate --certificate-arn <CERT_ARN> \
  --region <REGION> --profile <PROFILE> \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'
```

Wait for validation:

```bash theme={"dark"}
aws acm wait certificate-validated --certificate-arn <CERT_ARN> \
  --region <REGION> --profile <PROFILE>
```

### Ingress controller

Install the nginx ingress controller with AWS NLB and ACM SSL termination:

```bash theme={"dark"}
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-type"=nlb \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-scheme"=internet-facing \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-ssl-cert"="<ACM_CERT_ARN>" \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-ssl-ports"="443" \
  --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-backend-protocol"="tcp" \
  --set controller.service.targetPorts.https=http
```

Get the NLB hostname (you'll need it for the DNS records):

```bash theme={"dark"}
kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
```

## 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>`.

```bash theme={"dark"}
cat <<EOF > /tmp/dns-record.json
{
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "*.<DOMAIN>.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{ "Value": "<NLB_HOSTNAME>" }]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "*.chat.<DOMAIN>.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [{ "Value": "<NLB_HOSTNAME>" }]
      }
    }
  ]
}
EOF

aws route53 change-resource-record-sets \
  --hosted-zone-id <HOSTED_ZONE_ID> \
  --change-batch file:///tmp/dns-record.json \
  --profile <PROFILE>
```

This creates the following endpoints:

| Service | URL |
| - | - |
| API | `https://api.<DOMAIN>` |
| Chat | `https://chat.<DOMAIN>` |
| AI Gateway | `https://ai-gateway.<DOMAIN>` |
| Agent Controller | `https://agent-controller.<DOMAIN>` |
| Execution tier (`agent-worker`) | `https://agent-worker.<DOMAIN>` |
| MCP | `https://mcp.<DOMAIN>` |
| Code Runner | `https://code-runner.<DOMAIN>` |
| AWS Operator | `https://aws-operator.<DOMAIN>` |

Test it:

```bash theme={"dark"}
curl -sk https://api.<DOMAIN>/health
```

Not reachable, or certificate errors on chat URLs? See [Troubleshooting](https://docs.xpander.ai/self-hosted/hybrid-troubleshooting.md#ingress-and-networking).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.