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

# Helm Chart Reference

> Deploy KaireonAI to Kubernetes with Helm — chart values, deployment modes, monitoring, and network policies.

The KaireonAI Helm chart deploys the complete platform to any Kubernetes cluster — EKS, GKE, AKS, or self-managed. The chart includes the API server, worker, PostgreSQL, Redis, PgBouncer, Prometheus, Grafana, ingress, network policies, and RBAC resources.

## Overview

The chart packages everything needed for a production KaireonAI deployment:

| Component             | Description                                                              |
| --------------------- | ------------------------------------------------------------------------ |
| **API Deployment**    | Next.js application with health checks and HPA                           |
| **Worker Deployment** | BullMQ background job processor for pipelines and model retraining       |
| **Cron Tier**         | Seven K8s CronJobs that trigger the scheduled `/api/v1/cron/*` endpoints |
| **Outbox Publisher**  | Dedicated pod that polls `outbox_events` and publishes via the event bus |
| **ML Worker**         | Optional Python/FastAPI service for AI-powered analysis                  |
| **PostgreSQL**        | Internal StatefulSet or external managed database                        |
| **Redis**             | Internal StatefulSet or external managed cache                           |
| **PgBouncer**         | Connection pooling for PostgreSQL                                        |
| **Prometheus**        | Metrics collection with pre-configured scrape targets                    |
| **Grafana**           | 6 auto-provisioned dashboards                                            |
| **Ingress**           | HTTPS with AWS ALB or nginx ingress controller                           |
| **NetworkPolicies**   | Pod-to-pod and egress traffic restrictions                               |
| **RBAC**              | ServiceAccounts, Roles, and RoleBindings                                 |

### Prerequisites

* Kubernetes 1.24+
* Helm 3.x
* `kubectl` configured for your cluster
* Container images pushed to a registry accessible from the cluster

## Quick Start

<Steps>
  <Step title="Add the chart">
    Clone the KaireonAI repository which includes the Helm chart in `helm/`:

    ```bash theme={null}
    git clone https://github.com/kaireonai/platform.git
    cd platform
    ```
  </Step>

  <Step title="Install with minimal values">
    For a quick test deployment with everything in-cluster:

    ```bash theme={null}
    helm install kaireon ./helm \
      -f helm/values-minimal.yaml \
      -n kaireon --create-namespace --wait
    ```
  </Step>

  <Step title="Verify the deployment">
    ```bash theme={null}
    kubectl get pods -n kaireon
    kubectl get svc -n kaireon
    ```
  </Step>
</Steps>

Or use the deploy script which handles namespace creation and secret generation:

```bash theme={null}
./scripts/deploy.sh
```

## Deployment Modes

KaireonAI supports three deployment modes depending on your environment and requirements.

<AccordionGroup>
  <Accordion title="Dev Mode — Single replicas, all in-cluster">
    Everything runs inside the cluster with minimal resources. Uses the internal PostgreSQL and Redis StatefulSets. No external dependencies required.

    **Best for:** Local development, CI testing, quick demos.

    ```bash theme={null}
    # Using the deploy script
    ./scripts/deploy.sh

    # Or directly with Helm
    helm install kaireon ./helm -f helm/values-minimal.yaml -n kaireon --create-namespace
    ```

    **Resource footprint:** Fits on a single `t3.medium` node (\~2 vCPU, 4 GiB).

    | Service                 | Estimated 7-Day Cost |
    | ----------------------- | -------------------- |
    | EKS Control Plane       | \$16.80              |
    | EC2 Node (1x t3.medium) | \$7.00               |
    | ALB                     | \$4.20               |
    | EBS Storage (\~10 GiB)  | \$1.00               |
    | **Total**               | **\~\$29**           |

    Key overrides in `values-minimal.yaml`:

    * API and worker: 1 replica each
    * HPA and KEDA disabled
    * Prometheus and Grafana disabled
    * Reduced resource requests (256Mi memory, 250m CPU)
    * Smaller PVC sizes (5 GiB database, 2 GiB Redis)
  </Accordion>

  <Accordion title="App Mode — External database and Redis, production-ready">
    Connect to managed services (RDS, ElastiCache, Cloud SQL, etc.) while running the application tier in Kubernetes. This is the recommended mode for production.

    **Best for:** Staging and production environments.

    ```bash theme={null}
    ./scripts/deploy.sh --mode=app --config=my-config.yaml
    ```

    Requires a config file specifying external database and Redis endpoints. See the [Example Configurations](#example-configurations) section below.
  </Accordion>

  <Accordion title="Full Mode — All components including monitoring stack">
    Deploys everything including Prometheus and Grafana with 6 auto-provisioned dashboards. Can use either internal or external database and Redis.

    **Best for:** Production environments where you want the full observability stack deployed alongside the application.

    ```bash theme={null}
    helm upgrade --install kaireon ./helm -n kaireon --create-namespace --wait
    ```

    The default `values.yaml` enables the full mode with 3 API replicas, 2 worker replicas, HPA, KEDA autoscaling, Prometheus, and Grafana.
  </Accordion>
</AccordionGroup>

## Values Reference

All configurable values are defined in `helm/values.yaml`. The sections below document each configuration group.

### Global

| Key         | Type   | Default   | Description                            |
| ----------- | ------ | --------- | -------------------------------------- |
| `namespace` | string | `kaireon` | Kubernetes namespace for all resources |

### API

<Accordion title="api.* — API deployment configuration">
  | Key                               | Type   | Default                     | Description                          |
  | --------------------------------- | ------ | --------------------------- | ------------------------------------ |
  | `api.image.repository`            | string | ECR repo                    | Container image repository           |
  | `api.image.tag`                   | string | `"082a1e2"`                 | Image tag                            |
  | `api.image.pullPolicy`            | string | IfNotPresent                | Image pull policy (Kubernetes enum)  |
  | `api.replicas`                    | int    | `3`                         | Number of API replicas               |
  | `api.resources.requests.memory`   | string | `"512Mi"`                   | Memory request                       |
  | `api.resources.requests.cpu`      | string | `"500m"`                    | CPU request                          |
  | `api.resources.limits.memory`     | string | `"2Gi"`                     | Memory limit                         |
  | `api.resources.limits.cpu`        | string | `"2000m"`                   | CPU limit                            |
  | `api.hpa.enabled`                 | bool   | `true`                      | Enable Horizontal Pod Autoscaler     |
  | `api.hpa.minReplicas`             | int    | `3`                         | Minimum replicas                     |
  | `api.hpa.maxReplicas`             | int    | `20`                        | Maximum replicas                     |
  | `api.hpa.targetCPUUtilization`    | int    | `70`                        | CPU target percentage for scaling    |
  | `api.hpa.targetMemoryUtilization` | int    | `80`                        | Memory target percentage for scaling |
  | `api.env.NEXTAUTH_URL`            | string | `"https://app.kaireon.com"` | Public URL for NextAuth callbacks    |

  The HPA includes scale-up/down behavior policies:

  * **Scale up:** stabilization window of 60s, add up to 4 pods per 60s
  * **Scale down:** stabilization window of 300s, remove up to 10% of pods per 60s
</Accordion>

### Worker

<Accordion title="worker.* — Worker deployment configuration">
  | Key                                | Type   | Default     | Description                       |
  | ---------------------------------- | ------ | ----------- | --------------------------------- |
  | `worker.image.repository`          | string | ECR repo    | Worker image repository           |
  | `worker.image.tag`                 | string | `"082a1e2"` | Image tag                         |
  | `worker.replicas`                  | int    | `2`         | Number of worker replicas         |
  | `worker.resources.requests.memory` | string | `"1Gi"`     | Memory request                    |
  | `worker.resources.requests.cpu`    | string | `"1000m"`   | CPU request                       |
  | `worker.resources.limits.memory`   | string | `"4Gi"`     | Memory limit                      |
  | `worker.resources.limits.cpu`      | string | `"4000m"`   | CPU limit                         |
  | `worker.keda.enabled`              | bool   | `true`      | Enable KEDA-based autoscaling     |
  | `worker.keda.minReplicas`          | int    | `1`         | Minimum worker replicas           |
  | `worker.keda.maxReplicas`          | int    | `10`        | Maximum worker replicas           |
  | `worker.keda.queueThreshold`       | string | `"5"`       | Queue depth threshold for scaling |
</Accordion>

### ML Worker

<Accordion title="mlWorker.* — ML Worker deployment configuration (optional)">
  | Key                                  | Type   | Default    | Description                  |
  | ------------------------------------ | ------ | ---------- | ---------------------------- |
  | `mlWorker.enabled`                   | bool   | `false`    | Deploy the ML Worker         |
  | `mlWorker.image.repository`          | string | ECR repo   | ML Worker image repository   |
  | `mlWorker.image.tag`                 | string | `"latest"` | Image tag                    |
  | `mlWorker.replicas`                  | int    | `1`        | Number of ML Worker replicas |
  | `mlWorker.resources.requests.memory` | string | `"1Gi"`    | Memory request               |
  | `mlWorker.resources.requests.cpu`    | string | `"500m"`   | CPU request                  |
  | `mlWorker.resources.limits.memory`   | string | `"4Gi"`    | Memory limit                 |
  | `mlWorker.resources.limits.cpu`      | string | `"2000m"`  | CPU limit                    |

  When enabled, the chart automatically injects `ML_WORKER_URL` into the API pods.
</Accordion>

### Config

<Accordion title="config.* — Application configuration">
  | Key                           | Type   | Default        | Description                                                                                                                                                                                |
  | ----------------------------- | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `config.LOG_LEVEL`            | string | `"info"`       | Log level: `debug`, `info`, `warn`, `error`                                                                                                                                                |
  | `config.WORKER_CONCURRENCY`   | string | `"5"`          | Concurrent jobs per worker pod                                                                                                                                                             |
  | `config.NODE_ENV`             | string | `"production"` | Node.js environment                                                                                                                                                                        |
  | `config.CORS_ALLOWED_ORIGINS` | string | `""`           | **Required at install when `NODE_ENV=production`.** Comma-separated list of allowed browser origins for the API. Must be an explicit, non-wildcard value (e.g. `https://app.example.com`). |
  | `config.EVENT_PUBLISHER`      | string | `"redis"`      | Event bus backend: `redis`, `kafka`, `msk`, `eventbridge`, `kinesis`                                                                                                                       |
  | `config.INTERACTION_STORE`    | string | `"pg"`         | Interaction history store: `pg`, `dynamodb`, `keyspaces`, `scylla`                                                                                                                         |
  | `config.SEARCH_INDEX`         | string | `"pg"`         | Search index backend: `pg`, `opensearch`                                                                                                                                                   |

  <Note>
    `config.CORS_ALLOWED_ORIGINS` ships empty on purpose. With `NODE_ENV=production` (the default) the API's startup env-validation refuses to boot unless it is set to an explicit, non-wildcard origin, so the chart **fails at `helm install` / `helm template` time** with an actionable message rather than letting the pod crash-loop. Supply your app's public origin(s) at install:

    ```bash theme={null}
    helm install kaireon ./helm \
      --set config.CORS_ALLOWED_ORIGINS=https://app.example.com \
      -n kaireon --create-namespace
    ```

    For multiple origins, escape the commas so Helm treats them as one string value: `--set config.CORS_ALLOWED_ORIGINS=https://app.example.com\,https://admin.example.com` (or set it in a `-f` values override, where commas need no escaping). `scripts/deploy.sh` reads a `CORS_ALLOWED_ORIGINS` env var and defaults to `http://localhost:3000` for its dev-mode path.
  </Note>
</Accordion>

### Database

<Accordion title="database.* — PostgreSQL configuration">
  Set `database.mode` to control how PostgreSQL is provisioned:

  * **`internal`** — Deploys a PostgreSQL 16 StatefulSet inside the cluster
  * **`external`** — Connects to a managed database (RDS, Cloud SQL, Supabase, etc.)

  **Internal mode values:**

  | Key                           | Type   | Default                  | Description                                       |
  | ----------------------------- | ------ | ------------------------ | ------------------------------------------------- |
  | `database.internal.image`     | string | `postgres:16-alpine`     | PostgreSQL image                                  |
  | `database.internal.storage`   | string | `10Gi`                   | PVC storage size                                  |
  | `database.internal.username`  | string | `kaireon`                | Database user                                     |
  | `database.internal.password`  | string | `""`                     | Password (auto-generated 32-char random if empty) |
  | `database.internal.database`  | string | `kaireon`                | Database name                                     |
  | `database.internal.resources` | object | `256Mi/250m - 1Gi/1000m` | Resource requests/limits                          |

  **External mode values:**

  | Key                                | Type   | Default    | Description                                 |
  | ---------------------------------- | ------ | ---------- | ------------------------------------------- |
  | `database.external.host`           | string | `""`       | Database hostname                           |
  | `database.external.port`           | int    | `5432`     | Database port                               |
  | `database.external.name`           | string | `kaireon`  | Database name                               |
  | `database.external.username`       | string | `""`       | Database user                               |
  | `database.external.password`       | string | `""`       | Database password                           |
  | `database.external.sslMode`        | string | `require`  | SSL mode: `require`, `no-verify`, `disable` |
  | `database.external.existingSecret` | string | `""`       | Use existing K8s secret instead of password |
  | `database.external.secretKey`      | string | `password` | Key within the existing secret              |
</Accordion>

### Secrets

<Accordion title="secrets.* — Application secrets">
  | Key                                | Type   | Default      | Description                                                           |
  | ---------------------------------- | ------ | ------------ | --------------------------------------------------------------------- |
  | `secrets.provider`                 | string | `kubernetes` | Secrets backend                                                       |
  | `secrets.NEXTAUTH_SECRET`          | string | `""`         | NextAuth session encryption key                                       |
  | `secrets.JWT_SIGNING_SECRET`       | string | `""`         | JWT signing key                                                       |
  | `secrets.CONNECTOR_ENCRYPTION_KEY` | string | `""`         | Encryption key for stored connector credentials                       |
  | `secrets.WEBHOOK_SIGNING_SECRET`   | string | `""`         | HMAC secret for signing/verifying webhook + journey-callback payloads |
  | `secrets.API_KEY_PEPPER`           | string | `""`         | HMAC pepper for hashing API keys and OAuth client secrets             |

  With `secrets.provider: kubernetes` the chart templates the `kaireon-secrets` Secret with the keys above plus the derived `DATABASE_URL` / `REDIS_URL` (and `GF_ADMIN_PASSWORD` when Grafana is enabled). Set `secrets.provider: aws-secrets-manager` to pull the same keys from an `ExternalSecret` instead.

  <Note>
    `WEBHOOK_SIGNING_SECRET` and `API_KEY_PEPPER` are both required when `NODE_ENV=production` — the API refuses to start without them. The chart now templates both keys (in `secrets.yaml` for the `kubernetes` provider and `external-secret.yaml` for `aws-secrets-manager`), and `scripts/deploy.sh` auto-generates them alongside the other secrets. Provide your own values via `--set secrets.WEBHOOK_SIGNING_SECRET=…` / `--set secrets.API_KEY_PEPPER=…`, a values override, or your external secrets manager.
  </Note>

  <Warning>
    Generate secrets with `openssl rand -base64 32`. Never commit plaintext secrets to version control. Use `--set` flags, sealed secrets, or an external secrets manager in production.
  </Warning>
</Accordion>

### Redis

<Accordion title="redis.* — Redis configuration">
  Set `redis.mode` to control how Redis is provisioned:

  * **`internal`** — Deploys a Redis 7 StatefulSet inside the cluster
  * **`external`** — Connects to a managed Redis (ElastiCache, Upstash, etc.)

  **Internal mode values:**

  | Key                               | Type   | Default                  | Description              |
  | --------------------------------- | ------ | ------------------------ | ------------------------ |
  | `redis.internal.enabled`          | bool   | `true`                   | Deploy Redis StatefulSet |
  | `redis.internal.image.repository` | string | `redis`                  | Redis image              |
  | `redis.internal.image.tag`        | string | `7-alpine`               | Redis image tag          |
  | `redis.internal.storage`          | string | `10Gi`                   | PVC storage size         |
  | `redis.internal.maxmemory`        | string | `"512mb"`                | Redis max memory         |
  | `redis.internal.resources`        | object | `256Mi/250m - 1Gi/1000m` | Resource requests/limits |

  **External mode values:**

  | Key                             | Type   | Default    | Description                                 |
  | ------------------------------- | ------ | ---------- | ------------------------------------------- |
  | `redis.external.host`           | string | `""`       | Redis hostname                              |
  | `redis.external.port`           | int    | `6379`     | Redis port                                  |
  | `redis.external.tls`            | bool   | `true`     | Enable TLS                                  |
  | `redis.external.password`       | string | `""`       | Redis password                              |
  | `redis.external.existingSecret` | string | `""`       | Use existing K8s secret instead of password |
  | `redis.external.secretKey`      | string | `password` | Key within the existing secret              |
</Accordion>

### PgBouncer

<Accordion title="pgbouncer.* — Connection pooling configuration">
  Defined in `helm/values.yaml:209-225`. PgBouncer fronts PostgreSQL so the API tier shares a small pool against the database while individual Next.js request handlers can each open their own client connection.

  | Key                                   | Type   | Default             | Description                                          |
  | ------------------------------------- | ------ | ------------------- | ---------------------------------------------------- |
  | `pgbouncer.enabled`                   | bool   | `true`              | Deploy PgBouncer connection pooler                   |
  | `pgbouncer.image.repository`          | string | `edoburu/pgbouncer` | PgBouncer image                                      |
  | `pgbouncer.image.tag`                 | string | `"1.22.0"`          | PgBouncer version                                    |
  | `pgbouncer.poolMode`                  | string | `transaction`       | Pool mode: `transaction`, `session`, `statement`     |
  | `pgbouncer.defaultPoolSize`           | int    | `25`                | Connections per user/database pair                   |
  | `pgbouncer.maxClientConn`             | int    | `1000`              | Max client connections accepted from API/worker pods |
  | `pgbouncer.maxDbConnections`          | int    | `25`                | Max server connections opened to PostgreSQL          |
  | `pgbouncer.resources.requests.memory` | string | `"64Mi"`            | Memory request                                       |
  | `pgbouncer.resources.requests.cpu`    | string | `"100m"`            | CPU request                                          |
  | `pgbouncer.resources.limits.memory`   | string | `"256Mi"`           | Memory limit                                         |
  | `pgbouncer.resources.limits.cpu`      | string | `"500m"`            | CPU limit                                            |

  <Info>
    Transaction pooling (`transaction` mode) is recommended for Next.js applications. It allows multiple clients to share database connections between transactions, significantly reducing the number of connections to PostgreSQL.
  </Info>
</Accordion>

### Ingress

<Accordion title="ingress.* — Ingress and TLS configuration">
  | Key                 | Type   | Default           | Description                           |
  | ------------------- | ------ | ----------------- | ------------------------------------- |
  | `ingress.enabled`   | bool   | `true`            | Create Ingress resource               |
  | `ingress.className` | string | `alb`             | Ingress class: `alb` (AWS) or `nginx` |
  | `ingress.host`      | string | `app.kaireon.com` | Hostname for the application          |

  **AWS ALB Ingress** (when `className: alb`):

  | Key                          | Type   | Default           | Description                      |
  | ---------------------------- | ------ | ----------------- | -------------------------------- |
  | `ingress.aws.certificateArn` | string | `""`              | ACM certificate ARN for HTTPS    |
  | `ingress.aws.scheme`         | string | `internet-facing` | `internet-facing` or `internal`  |
  | `ingress.aws.targetType`     | string | `ip`              | `ip` (Fargate/CNI) or `instance` |
  | `ingress.aws.wafAclArn`      | string | `""`              | Optional WAF WebACL ARN          |

  **Nginx Ingress** (when `className: nginx`):

  | Key                         | Type   | Default            | Description                    |
  | --------------------------- | ------ | ------------------ | ------------------------------ |
  | `ingress.tls.enabled`       | bool   | `true`             | Enable TLS                     |
  | `ingress.tls.secretName`    | string | `kaireon-tls`      | TLS secret name                |
  | `ingress.tls.clusterIssuer` | string | `letsencrypt-prod` | cert-manager ClusterIssuer     |
  | `ingress.annotations`       | object | `{}`               | Additional Ingress annotations |
</Accordion>

### DNS

<Accordion title="externalDns.* — Route53 via external-dns">
  Defined in `helm/values.yaml:246-249`. When enabled, the chart adds the annotations external-dns expects on the Ingress so Route53 records track the cluster automatically. The companion `external-dns` controller is **not** installed by this chart — operators run it once per cluster from the upstream Helm chart.

  | Key                        | Type   | Default   | Description                                                                   |
  | -------------------------- | ------ | --------- | ----------------------------------------------------------------------------- |
  | `externalDns.enabled`      | bool   | `false`   | Auto-create Route53 records for the Ingress hostname                          |
  | `externalDns.hostedZoneId` | string | `""`      | Route53 hosted zone ID (a 14-character ID beginning with `Z`)                 |
  | `externalDns.txtOwnerId`   | string | `kaireon` | TXT record owner — disambiguates ownership when several clusters share a zone |
</Accordion>

### Monitoring

<Accordion title="monitoring.* — Prometheus and Grafana">
  | Key                                | Type   | Default    | Description                                     |
  | ---------------------------------- | ------ | ---------- | ----------------------------------------------- |
  | `monitoring.prometheus.enabled`    | bool   | `true`     | Deploy Prometheus                               |
  | `monitoring.prometheus.image.tag`  | string | `v2.51.0`  | Prometheus version                              |
  | `monitoring.prometheus.retention`  | string | `7d`       | Metrics retention period                        |
  | `monitoring.grafana.enabled`       | bool   | `true`     | Deploy Grafana with auto-provisioned dashboards |
  | `monitoring.grafana.image.tag`     | string | `"10.4.1"` | Grafana version                                 |
  | `monitoring.grafana.adminUser`     | string | `admin`    | Grafana admin username                          |
  | `monitoring.grafana.adminPassword` | string | `""`       | Grafana admin password                          |

  <Warning>
    Set the Grafana admin password via `--set` or a sealed secret in production. Never leave it empty in a publicly accessible deployment.
  </Warning>
</Accordion>

### Event Bus (Optional)

Set `config.EVENT_PUBLISHER` to activate an event bus backend. The default is `redis` which requires no extra configuration.

<Accordion title="kafka.* / msk.* / eventbridge.* / kinesis.* — Event bus backends">
  **Kafka:**

  | Key                   | Type   | Default              | Description                                                  |
  | --------------------- | ------ | -------------------- | ------------------------------------------------------------ |
  | `kafka.enabled`       | bool   | `false`              | Enable Kafka                                                 |
  | `kafka.brokers`       | string | `""`                 | Broker addresses (`broker1:9092,broker2:9092`)               |
  | `kafka.clientId`      | string | `"kaireon-platform"` | Kafka client ID                                              |
  | `kafka.tlsEnabled`    | bool   | `false`              | Enable TLS                                                   |
  | `kafka.saslMechanism` | string | `""`                 | SASL auth: `none`, `plain`, `scram-sha-256`, `scram-sha-512` |

  **Amazon MSK:**

  | Key            | Type   | Default       | Description                      |
  | -------------- | ------ | ------------- | -------------------------------- |
  | `msk.enabled`  | bool   | `false`       | Enable MSK                       |
  | `msk.brokers`  | string | `""`          | MSK broker endpoints             |
  | `msk.region`   | string | `"eu-west-2"` | AWS region                       |
  | `msk.authMode` | string | `"iam_role"`  | Auth: `iam_role` or `sasl_scram` |

  **Amazon EventBridge:**

  | Key                    | Type   | Default            | Description                      |
  | ---------------------- | ------ | ------------------ | -------------------------------- |
  | `eventbridge.enabled`  | bool   | `false`            | Enable EventBridge               |
  | `eventbridge.region`   | string | `"eu-west-2"`      | AWS region                       |
  | `eventbridge.busName`  | string | `"kaireon-events"` | Event bus name                   |
  | `eventbridge.authMode` | string | `"iam_role"`       | Auth: `iam_role` or `access_key` |

  **Amazon Kinesis:**

  | Key                    | Type   | Default            | Description    |
  | ---------------------- | ------ | ------------------ | -------------- |
  | `kinesis.enabled`      | bool   | `false`            | Enable Kinesis |
  | `kinesis.region`       | string | `"eu-west-2"`      | AWS region     |
  | `kinesis.streamName`   | string | `"kaireon-events"` | Stream name    |
  | `kinesis.partitionKey` | string | `"tenantId"`       | Partition key  |
</Accordion>

### Interaction Store (Optional)

Set `config.INTERACTION_STORE` to activate an alternative interaction history backend. The default is `pg` (PostgreSQL).

<Accordion title="dynamodb.* / keyspaces.* / scylla.* — Interaction store backends">
  **DynamoDB:**

  | Key                  | Type   | Default                  | Description                      |
  | -------------------- | ------ | ------------------------ | -------------------------------- |
  | `dynamodb.enabled`   | bool   | `false`                  | Enable DynamoDB                  |
  | `dynamodb.region`    | string | `"eu-west-2"`            | AWS region                       |
  | `dynamodb.tableName` | string | `"kaireon-interactions"` | Table name                       |
  | `dynamodb.authMode`  | string | `"iam_role"`             | Auth: `iam_role` or `access_key` |

  **Amazon Keyspaces:**

  | Key                  | Type   | Default       | Description      |
  | -------------------- | ------ | ------------- | ---------------- |
  | `keyspaces.enabled`  | bool   | `false`       | Enable Keyspaces |
  | `keyspaces.region`   | string | `"eu-west-2"` | AWS region       |
  | `keyspaces.keyspace` | string | `"kaireon"`   | Keyspace name    |

  **ScyllaDB:**

  | Key                        | Type   | Default         | Description                              |
  | -------------------------- | ------ | --------------- | ---------------------------------------- |
  | `scylla.enabled`           | bool   | `false`         | Enable ScyllaDB                          |
  | `scylla.contactPoints`     | string | `""`            | Contact points (`host1:9042,host2:9042`) |
  | `scylla.localDatacenter`   | string | `"datacenter1"` | Local datacenter                         |
  | `scylla.keyspace`          | string | `"kaireon"`     | Keyspace name                            |
  | `scylla.replicationFactor` | int    | `3`             | Replication factor                       |
</Accordion>

### Search Index (Optional)

Set `config.SEARCH_INDEX` to activate an alternative search backend. The default is `pg` (PostgreSQL tsvector).

<Accordion title="opensearch.* — OpenSearch configuration">
  | Key                      | Type   | Default      | Description                 |
  | ------------------------ | ------ | ------------ | --------------------------- |
  | `opensearch.enabled`     | bool   | `false`      | Enable OpenSearch           |
  | `opensearch.nodeUrl`     | string | `""`         | OpenSearch endpoint         |
  | `opensearch.authMode`    | string | `"basic"`    | Auth: `basic` or `iam_role` |
  | `opensearch.indexPrefix` | string | `"kaireon-"` | Index name prefix           |
</Accordion>

### Cron Tier

<Accordion title="cron.* — scheduled CronJob triggers">
  When `cron.enabled: true` (the default) the chart creates one Kubernetes CronJob per entry in `cron.schedules`. Each job runs a small `curlimages/curl` container that fires a single authenticated `POST` — `curl -H "Authorization: Bearer $CRON_SECRET" <baseUrl><path>` — at the in-cluster API service; the actual work runs inside the API tier.

  | Key                     | Type   | Default                                        | Description                                  |
  | ----------------------- | ------ | ---------------------------------------------- | -------------------------------------------- |
  | `cron.enabled`          | bool   | `true`                                         | Create the CronJob resources                 |
  | `cron.image.repository` | string | `curlimages/curl`                              | Image used to fire the HTTP trigger          |
  | `cron.image.tag`        | string | `"8.10.1"`                                     | Image tag                                    |
  | `cron.baseUrl`          | string | `http://kaireon-api.kaireon.svc.cluster.local` | In-cluster URL of the API service            |
  | `cron.schedules`        | map    | see below                                      | `name → { enabled, schedule, path }` entries |

  Seven schedules ship enabled by default:

  | Schedule                    | Cron           | Endpoint                                   |
  | --------------------------- | -------------- | ------------------------------------------ |
  | `cleanup`                   | `30 4 * * *`   | `/api/v1/cron/cleanup`                     |
  | `engagementHealthRecompute` | `15 3 * * *`   | `/api/v1/cron/engagement-health-recompute` |
  | `gitopsDriftCheck`          | `0 5 * * *`    | `/api/v1/cron/gitops-drift-check`          |
  | `approvalsExpire`           | `*/15 * * * *` | `/api/v1/cron/approvals-expire`            |
  | `flowSchedulerTick`         | `* * * * *`    | `/api/v1/cron/flow-scheduler-tick`         |
  | `scheduledRetrains`         | `0 2 * * *`    | `/api/v1/cron/scheduled-retrains`          |
  | `outboxReaper`              | `*/2 * * * *`  | `/api/v1/cron/outbox-reaper`               |

  <Info>
    This tier is for multi-replica deployments. On a single replica the API container already runs an **in-process maintenance scheduler** and **flow scheduler** that fire these jobs on their own once `CRON_SECRET` (or `CRON_TOKEN`) is set — set `cron.enabled: false` to rely on those instead, or disable the in-process schedulers (`MAINTENANCE_SCHEDULER_ENABLED=false`, `FLOW_INTERNAL_SCHEDULER_ENABLED=false`) when you prefer the CronJobs. The API tier must have `CRON_SECRET` available for the jobs to authenticate.
  </Info>
</Accordion>

### Outbox Publisher

<Accordion title="outboxPublisher.* — dedicated outbox relay pod">
  When `outboxPublisher.enabled: true` (the default) the chart deploys a dedicated pod that polls the `outbox_events` table and publishes each event through the configured `EVENT_PUBLISHER`. Splitting it from the main worker tier keeps outbox tail-latency independent of long-running batch jobs.

  | Key                                             | Type   | Default                       | Description                                             |
  | ----------------------------------------------- | ------ | ----------------------------- | ------------------------------------------------------- |
  | `outboxPublisher.enabled`                       | bool   | `true`                        | Deploy the outbox publisher pod                         |
  | `outboxPublisher.replicas`                      | int    | `1`                           | Number of publisher replicas                            |
  | `outboxPublisher.pollIntervalMs`                | int    | `2000`                        | Poll cadence in milliseconds                            |
  | `outboxPublisher.shutdownDrainTimeoutMs`        | int    | `15000`                       | Graceful-shutdown drain budget                          |
  | `outboxPublisher.livenessFile`                  | string | `/tmp/outbox-publisher.alive` | File the loop touches every iteration                   |
  | `outboxPublisher.livenessProbe.maxStaleSeconds` | int    | `90`                          | Restart the pod if the liveness file is older than this |
  | `outboxPublisher.pdb.minAvailable`              | int    | `1`                           | PodDisruptionBudget minimum                             |
  | `outboxPublisher.terminationGracePeriodSeconds` | int    | `60`                          | Grace period on SIGTERM                                 |
</Accordion>

### Large-Topology Aliases

The following keys appear only in the large-enterprise overlay at `helm/values-large.yaml`. The base chart (`helm/values.yaml`) does not define them — they are layered in via `helm upgrade -f helm/values.yaml -f helm/values-large.yaml`. Each is a high-level alias that the chart maps onto the per-backend sections above. See [values-large.yaml — enterprise topology](/self-host/configure/values-large) for the full overlay.

<Accordion title="postgresExternalReplicas.* — Read-replica advisory count">
  Defined in `helm/values-large.yaml:163`. A single integer that records how many read replicas the operator has provisioned alongside the primary. Advisory only — the chart does not itself create RDS replicas; consumers (e.g. the read-only analytics path) read this number when deciding whether to fan out reads.

  | Key                        | Type | Default             | Description                                                                                     |
  | -------------------------- | ---- | ------------------- | ----------------------------------------------------------------------------------------------- |
  | `postgresExternalReplicas` | int  | `2` (large overlay) | Number of read replicas the chart caller has provisioned. Set to `0` when running primary-only. |

  `postgresExternalReplicas` is meaningful only when `database.mode: external` (or, in the large overlay, `postgres.mode: external`). For internal-StatefulSet deployments leave it unset.
</Accordion>

<Accordion title="eventbus.* — Event-bus backend selector">
  Defined in `helm/values-large.yaml:173-175`. A high-level alias that picks one of the per-backend blocks (`kafka.*`, `msk.*`, `eventbridge.*`, `kinesis.*`). The base chart instead drives this from `config.EVENT_PUBLISHER` (`redis | kafka | msk | eventbridge | kinesis`); the large overlay adds the alias for callers who prefer to read the choice as a single key. Either form is honoured — the operator should not set both unless the values agree.

  | Key                | Type   | Default                 | Description                                                                                                                                         |
  | ------------------ | ------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `eventbus.enabled` | bool   | `true` (large overlay)  | Whether an external event bus is in use. When `false`, the chart falls back to `config.EVENT_PUBLISHER: redis`.                                     |
  | `eventbus.backend` | string | `kafka` (large overlay) | One of `kafka`, `kinesis`, `pulsar`. Selecting a backend here still requires the matching per-backend block (e.g. `kafka.brokers`) to be filled in. |
</Accordion>

<Accordion title="interactionStore.* — Interaction-store backend selector">
  Defined in `helm/values-large.yaml:177-178`. The large-overlay alias for `config.INTERACTION_STORE`. Selects which write path receives the partitioned interaction-history rows. Each backend still requires its own per-backend block (`dynamodb.*`, `scylla.*`, `keyspaces.*`) to be filled in.

  | Key                        | Type   | Default                    | Description                                                                                                                       |
  | -------------------------- | ------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
  | `interactionStore.backend` | string | `dynamodb` (large overlay) | One of `pg`, `dynamodb`, `scylla`, `cassandra` (mid-tier). The base chart drives the same choice from `config.INTERACTION_STORE`. |

  The chart does not provision the target store — the operator owns the DynamoDB table, Scylla cluster, or Keyspaces keyspace before flipping this value.
</Accordion>

## Grafana Dashboards

The chart includes 6 pre-built Grafana dashboards that are auto-provisioned from `helm/dashboards/`. When `monitoring.grafana.enabled=true`, these dashboards are available immediately after deployment.

| Dashboard                | File                        | Key Panels                                                                           |
| ------------------------ | --------------------------- | ------------------------------------------------------------------------------------ |
| **API Overview**         | `api-overview.json`         | Request rates, error rates, latency percentiles (p50/p95/p99), HTTP status breakdown |
| **Decision Engine**      | `decision-engine.json`      | Pipeline stage durations, candidate counts, scoring latency, cache hit rates         |
| **Decision Performance** | `decision-performance.json` | Scoring model performance, qualification rates, conversion tracking, uplift metrics  |
| **Infrastructure**       | `infrastructure.json`       | CPU and memory utilization, disk I/O, network throughput, pod restarts               |
| **Model Health**         | `model-health.json`         | Model AUC tracking, drift detection, retraining triggers, prediction distributions   |
| **Worker Queues**        | `worker-queues.json`        | Queue depth, processing rates, job durations, DLQ counts, retry rates                |

<Info>
  KaireonAI exposes Prometheus metrics at `/api/metrics` (the bundled Prometheus deployment is pre-configured to scrape this path on port 3000; the endpoint requires an admin role or a valid API key). Key metrics include `kaireon_decision_latency_ms`, `kaireon_decision_delivery_total`, `kaireon_pipeline_execution_latency_ms`, `kaireon_http_request_duration_seconds`, and `kaireon_dlq_depth`.
</Info>

## Example Configurations

### Dev / Testing (Minimal)

Uses internal PostgreSQL and Redis with minimal resources. No external dependencies needed.

```yaml values-minimal.yaml theme={null}
api:
  replicas: 1
  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
    limits:
      memory: "1Gi"
      cpu: "1000m"
  hpa:
    enabled: false

worker:
  replicas: 1
  resources:
    requests:
      memory: "256Mi"
      cpu: "250m"
    limits:
      memory: "1Gi"
      cpu: "1000m"
  keda:
    enabled: false

config:
  WORKER_CONCURRENCY: "2"

database:
  internal:
    storage: 5Gi

redis:
  internal:
    storage: 2Gi
    maxmemory: "128mb"

monitoring:
  prometheus:
    enabled: false
  grafana:
    enabled: false
```

### EKS with RDS + ElastiCache

Production deployment on AWS with managed database and cache services.

<CodeGroup>
  ```yaml config.yaml theme={null}
  api:
    image:
      repository: <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/kaireon/api
      tag: "082a1e2"
    replicas: 3
    env:
      NEXTAUTH_URL: "https://app.yourdomain.com"

  ingress:
    enabled: true
    className: alb
    host: app.yourdomain.com
    aws:
      certificateArn: arn:aws:acm:us-east-1:123456789:certificate/abc-123

  database:
    mode: external
    external:
      host: kaireon-db.abc123.us-east-1.rds.amazonaws.com
      port: 5432
      name: kaireon
      username: kaireon
      existingSecret: kaireon-db-creds

  redis:
    mode: external
    external:
      host: kaireon-cache.abc123.0001.use1.cache.amazonaws.com
      port: 6379
      tls: true
      existingSecret: kaireon-redis-creds
  ```

  ```bash install.sh theme={null}
  helm upgrade --install kaireon ./helm \
    -n kaireon --create-namespace \
    -f config.yaml \
    --wait
  ```
</CodeGroup>

### GKE / Self-Managed with Nginx Ingress

Use nginx ingress controller with cert-manager for automatic TLS certificates.

```yaml values-gke.yaml theme={null}
ingress:
  className: nginx
  host: app.yourdomain.com
  tls:
    enabled: true
    clusterIssuer: letsencrypt-prod

database:
  mode: external
  external:
    host: kaireon-db.us-central1.cloudsql.google.com
    username: kaireon
    existingSecret: kaireon-db-creds

redis:
  mode: external
  external:
    host: 10.0.0.5
    port: 6379
    tls: false
```

### With Kafka and DynamoDB

Production deployment using Kafka for event streaming and DynamoDB for interaction history.

```yaml values-enterprise.yaml theme={null}
config:
  EVENT_PUBLISHER: kafka
  INTERACTION_STORE: dynamodb

kafka:
  enabled: true
  brokers: "broker1:9092,broker2:9092"
  tlsEnabled: true
  saslMechanism: scram-sha-256
  saslUsername: kaireon-user
  saslPassword: ""  # Use --set kafka.saslPassword=...

dynamodb:
  enabled: true
  region: us-east-1
  tableName: kaireon-interactions
  authMode: iam_role
```

## Upgrading

To upgrade an existing deployment to a new version:

```bash theme={null}
# Build and push new images, then upgrade
helm upgrade kaireon ./helm \
  -n kaireon \
  --set api.image.tag=$(git rev-parse --short HEAD) \
  --set worker.image.tag=$(git rev-parse --short HEAD) \
  --wait
```

<Warning>
  Database migrations run automatically on API pod startup via Prisma. Before upgrading, verify the migration is backward-compatible. If a migration requires downtime, scale down the API deployment first.
</Warning>

To roll back a failed upgrade:

```bash theme={null}
# View release history
helm history kaireon -n kaireon

# Roll back to previous revision
helm rollback kaireon <REVISION> -n kaireon
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Kubernetes Deployment" icon="dharmachakra" href="/self-host/deploy/kubernetes">
    Architecture overview, troubleshooting, and database options.
  </Card>

  <Card title="ML Worker" icon="microchip" href="/self-host/deploy/ml-worker">
    Configure the ML Worker for AI-powered analysis features.
  </Card>

  <Card title="Infrastructure Backends" icon="database" href="/self-host/deploy/infrastructure-backends">
    Configure Kafka, DynamoDB, OpenSearch, and other backends.
  </Card>

  <Card title="Cloud Deployment" icon="cloud" href="/self-host/deploy/cloud">
    One-click deployment to AWS App Runner.
  </Card>
</CardGroup>
