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

# Configuration

> Application settings, secrets, SMTP, and pointing the chart at infrastructure you already run.

# Configuration

Everything that isn't the database layout or ingress: how settings reach the
services, where secrets come from, and how to substitute your own
infrastructure.

***

## Application settings

Anything that would otherwise be a bespoke env var — encryption keys,
timeouts, log levels, model config — goes under `appSettings`, and lands as an
env var on every service.

Each entry takes one of three shapes:

```yaml theme={null}
appSettings:
  # 1. A plain value
  LOG_LEVEL: debug

  # 2. From a Secret you already have
  OPENAI_API_KEY:
    secretRef: my-openai-secret
    key: api-key

  # 3. From a ConfigMap you already have
  MODEL_CONFIG:
    configMapRef: llm-config
    key: models.json
```

These render to native `env:` entries — scalars as `value:`, maps as
`valueFrom.secretKeyRef` or `valueFrom.configMapKeyRef`. A map that is neither
shape fails the install rather than rendering something odd.

### Per-service overrides

`services.<name>.settings` takes the same three shapes and **wins** over
`appSettings` for the same key:

```yaml theme={null}
appSettings:
  LOG_LEVEL: info              # everything runs at info

services:
  relay:
    settings:
      LOG_LEVEL: debug         # ...except relay
      MODEL_CONFIG:
        configMapRef: llm-config
        key: models.json
```

<Note>
  The defaults ship `ENCRYPTION_KEY` and `AUDIT_HASHCHAIN_SIGNING_KEYS` wired to
  the chart-generated Secret, plus `LOG_LEVEL=info` — enough for a first install
  to work with no input. Override them with your own key management when you go
  to production.
</Note>

***

## Secrets

The chart generates a Secret named `swarmd-generated-credentials` at install:

| Key                                                   | Used by                                                                              |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `postgres-username` / `postgres-password`             | Every DB-backed service                                                              |
| `keycloak-admin-username` / `keycloak-admin-password` | Services calling the Keycloak Admin API                                              |
| `clickhouse-username` / `clickhouse-password`         | Populated even when ClickHouse is off, so enabling it later doesn't rotate anything  |
| `encryption-key`                                      | 32-character symmetric key shared by registry, relay, teams and the UI session store |

Passwords are 24 random alphanumeric characters. On `helm upgrade` the chart
reads the existing Secret via `lookup` and reuses the values — **upgrades do
not rotate credentials**.

The Secret carries `helm.sh/resource-policy: keep`, so `helm uninstall` leaves
it behind.

<Warning>
  **Back up `encryption-key` separately from your database.** It encrypts data
  at rest in registry, relay and teams. A database restore without the matching
  key leaves you with rows nobody can decrypt.
</Warning>

### Supplying your own

```yaml theme={null}
global:
  generatedSecrets:
    enabled: false
    name: swarmd-generated-credentials
```

Then pre-create a Secret of that name with the keys above. Useful when your
platform team owns secret material, or you sync from Vault or OpenBao with an
operator.

***

## SMTP

tenant-auth sends email for verification, password reset and team invites.
With `smtp.enabled: false` — the default — every one of those is **logged and
dropped**. Users can register, but nothing arrives.

```yaml theme={null}
smtp:
  enabled: true
  host: smtp.postmarkapp.com
  port: 587
  from: "no-reply@swarmd.example.com"
  credentialsSecret: smtp-creds     # Secret with `username` and `password`
```

```bash theme={null}
kubectl -n swarmd create secret generic smtp-creds \
  --from-literal=username='<smtp-user>' \
  --from-literal=password='<smtp-pass>'
```

The install refuses to render with `smtp.enabled: true` and any of `host`,
`from` or `credentialsSecret` missing.

<Warning>
  Turning SMTP on also flips Keycloak's `verifyEmail` action on. Existing
  unverified users will be asked to verify at next login — worth knowing before
  you enable it on a deployment that already has people in it.
</Warning>

***

## Bring your own infrastructure

Every optional component follows the same three-state pattern:
`enabled: false` · `enabled: true, deploy: true` · `enabled: true,
deploy: false` plus an `external` block. The chart fails the render if you
choose the third and leave a required URL empty.

### Bring your own Keycloak

```yaml theme={null}
keycloak:
  deploy: false
  external:
    serverUrl: http://keycloak.internal:8080        # in-cluster reachable
    publicUrl: https://auth.example.com             # what browsers see
    adminSecret: my-keycloak-admin                  # username + password keys
```

`adminSecret` holds master-realm admin credentials. Registry, relay,
tenant-auth and teams call the Admin API to manage agent service accounts, so
this is not optional.

<Warning>
  **External Postgres forces external Keycloak.** `keycloak.deploy=true` with
  `postgres.deploy=false` is unsupported and fails the render — the chart can't
  derive host and port from an arbitrary JDBC URL for the Keycloak container.
</Warning>

### PII detection

Both analyzers are **off by default**. With neither enabled, prompts pass
through the LLM path unfiltered.

```yaml theme={null}
presidio:
  enabled: true
  deploy: true            # in-cluster, pulled from Microsoft's public registry

piiranha:
  enabled: true
  deploy: false           # point at one you run
  external:
    url: http://piiranha.internal:5002
```

<Warning>
  **Piiranha cannot be deployed in-cluster from a licensed install.** Its
  analyzer image lives in `swarmd/piiranha-analyzer`, which is deliberately
  outside the release set — your licence grants pulls from `swarmd/releases/*`
  and `swarmd/charts/*` only.

  So `piiranha.deploy: true` schedules a pod that `ImagePullBackOff`s, while
  relay is handed a `PIIRANHA_ANALYZER_URL` pointing at a Service with no
  endpoints. Either run the analyzer yourself and use `deploy: false` with
  `external.url`, or point `piiranha.image.repository` at your own copy.

  Presidio has no such constraint — its image comes from Microsoft's public
  registry, so `deploy: true` works.
</Warning>

### a2a-payments

Off by default, and the only service that genuinely cannot start without
configuration: its `X402_WALLET_PRIVATE_KEY` is `@NotBlank`-validated with no
degraded mode.

```yaml theme={null}
services:
  a2aPayments:
    enabled: true
    settings:
      X402_WALLET_PRIVATE_KEY:
        secretRef: my-wallet
        key: private-key
```

### Turning services off

```yaml theme={null}
services:
  teams:
    enabled: false
  a2aPayments:
    enabled: false
```

Gateway, registry, relay, audit and tenant-auth are the core — disabling any
of them gives you a platform that doesn't work.

***

## Settings that bite later

Some settings don't block startup but fail the first time a feature is used.
The chart can't enforce them, because the service boots fine without them.

| Service    | Setting                                    | Symptom if missing                                               |
| ---------- | ------------------------------------------ | ---------------------------------------------------------------- |
| `teams`    | `TEAMS_BOT_APP_ID`, `TEAMS_BOT_APP_SECRET` | First Teams webhook returns `500`                                |
| `teams`    | `OPENAI_API_KEY`                           | LLM routing call fails                                           |
| `registry` | `MCP_OAUTH_REDIRECT_URI`                   | MCP dynamic client registration errors when an admin triggers it |

Provide them under `services.<name>.settings` when you enable the feature.

***

## Scaling: core and worker

Every DB-backed service ships as one image that can run three ways, and the
chart **splits every one of them by default**:

| Workload            | Runs                                                          | Sized by                                             |
| ------------------- | ------------------------------------------------------------- | ---------------------------------------------------- |
| `<service>`         | The API. `SWARMD_RUNTIME_MODE=core`.                          | `services.<name>.replicaCount` / `.resources`        |
| `<service>-worker`  | Schedulers and outbox drainers. `SWARMD_RUNTIME_MODE=worker`. | `services.<name>.worker.replicaCount` / `.resources` |
| `<service>-migrate` | Flyway, once, then exits. Runs before both.                   | —                                                    |

The worker's web server stays up so health probes work, but the Service never
routes traffic to it. Worker sizing falls back to the service's own
`replicaCount` / `resources` when left empty.

```yaml theme={null}
services:
  audit:
    replicaCount: 3            # three API pods
    worker:
      enabled: true
      replicaCount: 2          # two worker pods, sized independently
      resources:
        requests: { cpu: 500m, memory: 1Gi }
```

<Note>
  **Why this is the default.** In the combined topology, scaling the API to *N*
  replicas also runs *N* copies of every `@Scheduled` job — outbox drainers and
  monitor evaluators racing each other, which is a data problem rather than a
  performance one. Splitting also stops a slow scheduler starving the API's
  thread pool, and takes Flyway out of pod startup so a long migration can't
  trip readiness on every replica at once.
</Note>

### Collapsing a service back

```yaml theme={null}
services:
  teams:
    worker:
      enabled: false      # single container: API + schedulers + boot migration
```

Worth doing on a laptop, where the extra pod per service costs more than the
isolation buys you. Setting it on every service takes the default install
from 18 Deployments to 11.

<Warning>
  Don't run a collapsed service at `replicaCount > 1`. That is exactly the
  scheduler-duplication case the split exists to prevent.
</Warning>

***

## Observability

```yaml theme={null}
global:
  observability:
    prometheus:
      enabled: true
      path: /actuator/prometheus
    tracing:
      enabled: true
      endpoint: "http://otel-collector.observability:4317"
```

Prometheus scrape annotations land on every service pod; tracing points the
OTLP exporter at your collector.

***

## Verifying a change before you apply it

```bash theme={null}
helm lint deployments/h7s/swarmd

helm template swarmd deployments/h7s/swarmd \
  -f my-values.yaml \
  --set licence.key=LIC-TEST | kubectl apply --dry-run=client -f -
```

The chart's validation runs during `template`, so a bad combination fails here
— before it touches your cluster.

***

## Next

<CardGroup cols={2}>
  <Card title="Presets" icon="layer-group" href="/self-hosting/presets">
    Tested values files that combine all of this.
  </Card>

  <Card title="Databases" icon="database" href="/self-hosting/databases">
    Layouts, external Postgres, ClickHouse and backups.
  </Card>
</CardGroup>
