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

# Hosts, DNS and sign-in

> Publish an air-gapped install's hosts on your ingress, make them resolve inside your network, and sign in for the first time.

Set one `--domain` at [install](https://docs.xpander.ai/self-hosted/airgap-install.md) and the chart publishes every host on your ingress controller. Then make the hosts resolve where your users are and sign in with the bootstrap admin.

## Domains, ingress and DNS

One `--domain` (for example `xpander.acme.internal`) names every public host:

| Host | Serves |
| - | - |
| `chat.<domain>` | The web UI: the address your users open |
| `api.<domain>` | The REST API |
| `agent-controller.<domain>` | API calls and the UI's chat backend |
| `supabase.<domain>` | Auth, storage, and edge functions (the `supabase-kong` gateway) |
| `webhook.<domain>` | Webhook triggers |
| `client-auth.<domain>` | OAuth flows for skills, SSO login |
| `actions.<domain>` | The actions service |

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`.
* Rename a host with `<name>.ingress.host` (the UI also with `--ui-host`), and move the matching knobs in the same change.

> [accordion] Renaming hosts: the knobs that must follow each 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.
>
> | Host you rename | Knobs that must follow it |
> | - | - |
> | UI (`chat.` / `app.`) | `ui.ingress.host` or `--ui-host`, `supabase.auth.siteUrl`, `supabase.edgeRuntime.functionsEnv.APP_URL`, `client-auth.env.APP_URL`, `ui.env.VITE_PLATFORM_URL` |
> | Supabase gateway (`supabase.` / `auth.`) | `supabase.ingress.host`, `supabase.auth.apiExternalUrl`, `supabase.storage.publicUrl`, `supabase.edgeRuntime.publicUrl`, `global.assetStore.publicUrl`, `ui.env.VITE_SUPABASE_URL` |
> | API (`api.`) | `api.ingress.host`, `api.env.API_URL`, `ui.env.VITE_AGENTS_API_URL` |
> | agent-controller (`agent-controller.` / `chat.`) | `agent-controller.ingress.host`, `ui.env.VITE_CHAT_BACKEND_URL` (`https://<host>/chat-backend/{agent_id}`) |
> | Webhook (`webhook.` / `hooks.`) | `webhook.ingress.host`, `supabase.edgeRuntime.functionsEnv.INBOUND_GW_URL`, `ui.env.VITE_WEBHOOK_URL` |
> | Short links (`links.`, required when agents share links) | `functions.ingress.host`, `global.shortLinks.publicUrl` (its `/s/*` path must land on the `functions` Service) |
> | actions, client-auth | `actions.ingress.host`, `ui.env.VITE_ACTIONS_SERVICE_URL`, `client-auth.ingress.host`, `ui.env.VITE_CLIENT_AUTH_URL` |
> | deployment-manager (`dm.internal.`, required when a remote location connects) | `deployment-manager.ingress.host` and `className` (an internal class). `supabase.edgeRuntime.functionsEnv.DEPLOYMENT_MANAGER_PUBLIC_URL` |
> | Every install that sets hosts by hand | `supabase.edgeRuntime.functionsEnv.AGENT_CONTAINER_INGRESS_DOMAIN` (agent workspaces), `ui.env.VITE_LICENSE_MODE: "true"`, `ui.env.VITE_EMAIL_DOMAIN` when agents have mailboxes, and `ui.extraEnv` with `VITE_SUPABASE_ANON_KEY` read from the `xpander-generated-keys` Secret |
>
> A complete example with renamed hosts: [Reference values file](https://docs.xpander.ai/self-hosted/airgap-upgrade.md#reference-values-file).

> [accordion] TLS at your controller or at a cloud load balancer
>
> **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:
>
> ```yaml theme={"dark"}
> global:
>   ingress:
>     className: traefik
>     tls:
>       - hosts: ["*.xpander.acme.internal"]
>         secretName: xpander-wildcard-tls
> ```
>
> **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

**Sealed networks:** resolution must happen where your users are (internal zone or VPN DNS). The domain never needs to exist publicly.

> [tab] Internal DNS zone
>
> A wildcard in the zone your users resolve:
>
> ```
> *.xpander.acme.internal.  CNAME  <your-ingress-controller's address>
> ```
>
> Or one record per host when wildcards are not allowed.

> [tab] VPN + internal load balancer
>
> Expose the ingress controller on an **internal** load balancer reachable over the VPN. In a sealed VPC, tag private subnets `kubernetes.io/role/internal-elb=1` and annotate the Service `aws-load-balancer-internal`: commands in [Troubleshooting](https://docs.xpander.ai/self-hosted/airgap-operate.md#cluster-and-network).

> [tab] Local test: port-forward + /etc/hosts
>
> One port-forward to the ingress controller serves every host (it routes by `Host` header). **Forward to the real scheme port** (443 for https, 80 for http): xpander Chat's origins carry no port, so another port breaks every call after login. Binding 443/80 needs admin/root:
>
> ```bash theme={"dark"}
> sudo kubectl -n <ingress-namespace> port-forward svc/<controller-service> 443:443
> # installed with --ingress-scheme http, forward port 80 instead:
> sudo kubectl -n <ingress-namespace> port-forward svc/<controller-service> 80:80
> ```
>
> ```
> # /etc/hosts
> 127.0.0.1 chat.xpander.acme.internal api.xpander.acme.internal agent-controller.xpander.acme.internal supabase.xpander.acme.internal webhook.xpander.acme.internal client-auth.xpander.acme.internal actions.xpander.acme.internal
> ```
>
> Then open `https://chat.xpander.acme.internal` (or `http://…` for an `--ingress-scheme http` install), with no port.

## 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:

   ```bash theme={"dark"}
   kubectl -n xpander get secret xpander-generated-keys \
     -o jsonpath='{.data.XPANDER_ADMIN_PASSWORD}' | base64 -d
   ```

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](https://docs.xpander.ai/run/integrations/email.md#transactional-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](https://docs.xpander.ai/self-hosted/sso.md). Google and GitHub buttons stay GoTrue providers (`supabase.auth.extraEnv`).

No domain yet? Port-forward to the UI:

```bash theme={"dark"}
kubectl -n xpander port-forward svc/ui 8080:80   # then http://localhost:8080
```

**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.

## Next steps

> [card] [Configure the install](https://docs.xpander.ai/self-hosted/airgap-configure.md)
>
> Data tier, executor fleet, skill generation and skill credentials.

> [card] [Connect your models](https://docs.xpander.ai/self-hosted/airgap-models.md)
>
> Your LLM endpoint, Amazon Bedrock or a local vLLM server.


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