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

# External walkthrough

> A fully self-hosted install explained step by step, including what you take on by running the control plane yourself.

External mode moves the whole platform into your cluster. This page explains what that means in practice. For just the commands, see [Quick start](/neuraltrust/deployment/quickstart) and [External (self-hosted)](/neuraltrust/deployment/external).

## What changes relative to Hybrid

The data planes are the same binaries. What is added is everything that was previously operated for you.

|                               | Hybrid                        | External                            |
| ----------------------------- | ----------------------------- | ----------------------------------- |
| Console (`control-plane-app`) | NeuralTrust                   | **Yours**                           |
| Configuration store           | NeuralTrust                   | **Your PostgreSQL**                 |
| Config-sync server            | NeuralTrust SaaS endpoint     | **In-cluster, over TLS**            |
| Residency / metrics layer     | DataAgent → DataBridge (SaaS) | **DataCore reading ClickHouse**     |
| Config-sync token             | Issued by console             | **Not used**                        |
| DataAgent enrolment token     | Required                      | **Not used**                        |
| Database migrations           | NeuralTrust                   | **Yours, run by an init container** |
| First admin                   | Sign-up                       | **Bootstrap from configuration**    |
| ClickHouse                    | Not required                  | **Required**                        |

The token columns are the part people find surprising. In external mode there is no SaaS to authenticate to, so the two credentials that dominate a hybrid install simply do not exist. What replaces them is a set of internal JWT secrets that must match on both sides of every call — and the chart generates and aligns all of them.

### DataCore instead of DataAgent

In hybrid, DataAgent answers queries pushed from SaaS over an outbound stream. External mode has no SaaS, so that role disappears and **DataCore** takes over: an in-cluster HTTP API that serves tenant-scoped queries directly from ClickHouse, plus deployment metadata from its own `datacore` PostgreSQL database.

The console reaches it at `DATACORE_URL` with a JWT signed by a secret that must equal DataCore's `AUTH_JWT_HS256_SECRET`. The chart wires both ends from a single generated value. If you override one by hand you must override the other, or every dashboard returns an authorization error.

## Step 1: prepare the datastores

External mode needs three, and unlike hybrid, ClickHouse is mandatory.

| Store      | Usual choice                                    | Used for                                           | Notes                                                                                         |
| ---------- | ----------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| PostgreSQL | **Managed** in production; in-cluster for demos | Control-plane schema plus one database per service | Managed: pre-create roles yourself. In-cluster (chart 2.7.0+): the bootstrap Job creates them |
| ClickHouse | **In-cluster**                                  | Telemetry and every dashboard metric               | DataCore migrates its own tables at startup                                                   |
| Redis      | **Managed** in production; in-cluster for demos | Gateway state                                      | Required at boot                                                                              |

That split is deliberate. Postgres and Redis are almost always managed services in production, so the usual external values file sets `deploy: false` for both and names the endpoints once on `global.postgresql` / `global.redis` (per-service hosts inherit since chart 2.6.0). ClickHouse is the opposite: nearly everyone runs the in-cluster subchart, because it is telemetry storage the platform owns end to end. Connecting an external ClickHouse is supported but rare — treat it as the exception, not the starting point.

For managed Postgres, keep runtime role passwords in a Secret you create and reference with per-service `existingSecret` hooks — see
[External per-service datastore credentials](/neuraltrust/deployment/secrets#external-per-service-datastore-credentials).
Since chart 2.8.0 the control-plane `neuraltrust` password goes the same way, through `global.postgresql.passwordSecret`, so an external values file can hold no credential at all; before that it had to be readable at render time as `global.postgresql.password`, because the chart composed the console's connection string while rendering. Replacing `postgresql-secrets` wholesale with `global.postgresql.existingSecret` is a different contract (Hybrid / full Secret takeover) and is usually the worse path for External.

On AWS you can use RDS IAM authentication. Set `global.postgresql.authMode: iam`. Be aware of a current gap: that umbrella setting does not reach the Go gateways in external mode, so also set `agentgateway.database.iamAuth: true` and `trustguard.database.iamAuth: true`. Without them the gateways attempt password authentication against an IAM-only database and fail at connection time with nothing pointing at the cause.

## Step 2: decide the console hostname first

`APP_URL` and `NEXTAUTH_URL` drive email links, SSO redirects, and OAuth callbacks. Changing them later invalidates in-flight invitations and breaks configured SSO redirect URIs, so pick the hostname before installing.

One limitation to be aware of: a handful of console values are `NEXT_PUBLIC_*`, which Next.js inlines into the JavaScript bundle at **image build time**. They cannot be set by Helm. In practice only two matter, and both are cosmetic-to-annoying rather than blocking — the SCIM tenant URL shown to administrators and the `meta.location` returned to your IdP will show `app.neuraltrust.ai`. Enter your own host in your IdP rather than copying what the screen displays.

## Step 3: the bootstrap admin

There is no sign-up flow, and the database seed creates no users — it seeds the evaluator catalog and reference data only. The first administrator comes from two environment values:

```yaml theme={null}
global:
  superadmin:
    existingSecret:
      name: neuraltrust-superadmin
```

The feature activates only when `DEPLOYMENT_MODE=external` and both the email and password are non-empty. On password login the console checks these **before** normal user authentication, and on a match upserts the user with a verified email and grants OWNER on every team.

Treat it as a break-glass account: it bypasses the normal user table by design. Create a personal administrator through the UI afterwards.

## Step 4: install, and watch the ordering

Two things must complete before the console serves traffic:

1. A **pre-install Helm hook Job** generates the MCP OAuth RSA signing key into a Secret. It checks for an existing key first, so upgrades do not rotate it and invalidate live access tokens. It only ever writes its own Secret — the Role is name-scoped.
2. An **init container** runs `prisma migrate deploy` followed by the seed.

The console Deployment rolls with `maxUnavailable: 0`, so a failed migration blocks the rollout rather than serving against a half-migrated schema. Note that `control-plane-api` reads the *same* schema and runs no migrations of its own — it assumes the console has already applied them.

## Step 5: configuration comes from Postgres, not config sync

This is the step most likely to trip you up if you have deployed hybrid before.

In hybrid, a data plane holds no database connection for its own configuration and pulls snapshots from SaaS over gRPC. **External mode inverts that.** Config sync is off by default, and each product's data plane reads its configuration directly from that product's PostgreSQL database, which its own in-cluster control plane owns and migrates.

Practical consequences:

* There is no config-sync token to create and no snapshot cache to reason about
* Data-plane readiness has no snapshot gate, so `Running` and `Ready` track each other closely
* Each product needs its **own** database and role, not one shared credential — `agentgateway`, `trustguard`, `datacore`, plus the platform database

The chart still generates gRPC TLS material for the product control planes: a self-signed CA and a server certificate whose SANs cover the control-plane Service DNS name, preserved across upgrades. Nothing consumes it while config sync is off, but it means you can enable config sync later without provisioning certificates. If your policy requires your own PKI, supply a `kubernetes.io/tls` Secret including `ca.crt` through `configSync.grpcTls.existingSecret`; setting `autoGenerate: false` without one is rejected at render time.

## Step 6: verify

```bash theme={null}
kubectl -n neuraltrust get pods
kubectl -n neuraltrust logs deploy/datacore | grep -i 'migration\|listening'
kubectl -n neuraltrust logs deploy/agentgateway-admin | grep -i 'migration\|listening'
```

Then log in and create the organization.

## What you have taken on

Worth stating plainly, since it is the real cost of external mode:

* **Database migrations** are yours to run and to roll back
* **Backups** of PostgreSQL and ClickHouse are yours
* **Certificate rotation** for anything you supplied yourself
* **The bootstrap credential** is a standing break-glass path into every team
* **Email deliverability** — invitations and password resets stop working silently if it breaks

## Known rough edges

External mode is newer than Hybrid, and these are the issues you are most likely to meet. Each has a workaround in [Troubleshooting](/neuraltrust/deployment/troubleshooting).

* Login lockout after repeated failed password attempts
* SCIM setup screens display the SaaS host instead of yours
* Alert evaluation does not currently run
* Firewall complexity state silently disables itself without a Redis URL
* Trace export returns 404 when the OTLP endpoint includes a signal path
* `imagePullSecrets` is only honoured on the console under `control-plane-app`
* IAM database auth needs setting per gateway, not only globally
