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

# Migrate self-hosted FlowX.AI to the flowx-ai Helm chart

> Move self-hosted FlowX.AI from the per-service or ai-platform Helm charts to the single flowx-ai chart, keeping your databases, files, and qdrant data.

> [!NOTE]
>
> **Available starting with FlowX.AI 5.9.6.** The `flowx-ai` chart deploys the core and AI services as one Helm release.

> [!NOTE]
>
> In the FlowX.AI 6.x release family, the `flowx-ai` chart is the only supported way to deploy the platform. FlowX.AI 5.9.x still supports installing the core services with one Helm chart per service.

> [!WARNING]
>
> This page describes the `flowx-ai` chart as it ships in FlowX.AI 5.9.6, including which AI services get an admin Ingress. Migrate to the chart while you're on 5.9.x, then upgrade to 6.0.0, as described in the [Helm chart upgrade to 6.0.0](https://docs.flowx.ai/release-notes/v6.x/v6.0.0-october-2026/migrating-from-v5.9-to-v6.0/helm-chart.md).

## Overview

The migration replaces your platform workloads once. Your databases, stored files, and indexed qdrant collections are kept.

| | Before | After |
| - | - | - |
| **Core services** | One Helm release per service, each from its own FlowX chart | `services.<name>` entries in one `flowx-ai` release |
| **AI services** | The `ai-platform` umbrella chart, one sub-chart per service | `services.<name>` entries in the same release, switched on by group |
| **Values** | One values file per release. A `global` value reaches only its own release. | One values tree with `global`, `groups`, and `services` |
| **Workload labels** | `app.kubernetes.io/name: <service>` | `app.kubernetes.io/name: flowx-ai`, `app.kubernetes.io/instance: <release>`, `app.kubernetes.io/component: <service>`. The bundled qdrant keeps `app.kubernetes.io/name: qdrant`. |

### What stays the same?

* **Your data.** Databases, files in the object store, and qdrant collections carry over.
* **Datastores.** The chart does not install, change, or delete PostgreSQL, MongoDB, Redis, Elasticsearch, Kafka, or your object store.
* **Persistent volumes.** The qdrant volume claims keep their names and are reattached. See [PVC behavior](#pvc-behavior).
* **Resource names.** Deployments, Services, and Ingresses keep their per-service names, except for the objects listed in [Other behavior changes](#other-behavior-changes).

### What changes?

The workload labels change, and they are part of `spec.selector`. Kubernetes does not allow the selector of an existing Deployment or StatefulSet to change, so `helm upgrade` cannot move a workload from the old charts to the new one. Every Deployment must be deleted and recreated. The qdrant StatefulSet must be recreated too, unless the `flowx-ai` release keeps the release name of your `ai-platform` release: qdrant keeps its `app` and `app.kubernetes.io/name` labels, so its selector changes only through `app.kubernetes.io/instance`.

> [!WARNING]
>
> The services are unavailable from the time you delete the old workloads until the new pods are ready. Plan a maintenance window.

The `flowx-ai` chart deploys the platform version it was released with, so the migration also upgrades every service to that version. Before the window, read the upgrade notes of every release between your current version and the chart's version, including the [5.9.6 upgrade notes](https://docs.flowx.ai/release-notes/v5.x/v5.9.6-october-2026/v5.9.6-october-2026.md#upgrade-notes).

***

## Before you begin

### Support for the previous charts

* **`ai-platform` umbrella chart:** retired. From FlowX.AI 5.9.6, the AI services ship in the `flowx-ai` chart.
* **Per-service charts:** move to the `flowx-ai` chart so that later upgrades are a single `helm upgrade` of one release.

### Prerequisites

* Helm 3 and `kubectl`, with access to the namespace that runs FlowX.AI
* `jq` and `yq` v4 (the mikefarah Go version), for the commands in this guide
* Pull access to the `flowx-ai` chart in the chart registry FlowX provides for your installation, at the chart version for your target release. The chart version is the platform release, for example `5.9.6`.
* For the default Gateway API mode: the Gateway API CRDs (`gateway.networking.k8s.io/v1`) in the cluster, a Gateway controller, and a Gateway named `default-gateway` with an `https` listener in the FlowX.AI namespace. The chart attaches its HTTPRoutes to that Gateway but does not create it. To use an existing Gateway, set its name and namespace in `gatewayName` and `gatewayNamespace` under `global.flowx.gateway.public`, `global.flowx.gateway.admin`, and `global.flowx.gateway.adminInstances`. Without the CRDs the chart does not render and fails with `Gateway API HTTPRoute CRD (gateway.networking.k8s.io/v1) not found`. To use Kubernetes Ingress instead, see [Check the global settings](#check-the-global-settings).
* Image pull secrets for every service in the namespace, AI services included. The chart applies `imagePullSecrets` to all pods.
* The `flowx-events-gateway-sa` client and its secret in your identity provider realm. events-gateway authenticates with it in the `flowx-ai` chart.
* If SpiceDB already runs in your environment: `spicedb-operator.enabled: false` and `spicedb-cluster.enabled: false` in your values. The chart installs both by default. To move that SpiceDB into the chart later, see [Move an existing SpiceDB into the chart](#move-an-existing-spicedb-into-the-chart).
* A backup of PostgreSQL, MongoDB, the object store, and the qdrant collections, taken right before the maintenance window. It is your recovery point for a rollback.
* A maintenance window. Measure the delete-to-ready time in a test environment first.

The commands in this guide read the namespace from `NS`. Set it once in your shell:

```bash theme={"dark"}
export NS={your-flowx-namespace}
```

***

## Service groups

Every service in the chart belongs to exactly one group, set in `services.<name>.group`. A service with no group belongs to `base`. You turn a whole group on or off under `groups`. The groups replace the `devtime` and `runtime` flags of the `ai-platform` umbrella chart.

| Group | Purpose | Members |
| - | - | - |
| `core` | FlowX.AI platform services | admin, advancing-controller, application-manager, audit-core, authorization-system, cms-core, data-search, designer, document-plugin, email-gateway, events-gateway, file-gateway, integration-designer, license, nosql-db-runner, notification-plugin, organization-manager, process-engine, runtime-manager, scheduler-core, solution-portal, task-management-plugin, webhook-gateway |
| `base` | The AI foundation that the other AI services call | ai-gateway, doc-parser, embedder, knowledgebase-indexer-v2, knowledgebase-rag, and the bundled qdrant |
| `devtime` | AI assistance in FlowX Designer | ai-analyst, ai-architect, ai-assistant, ai-designer, ai-developer, kb-enrichment, planner |
| `runtime` | AI capabilities used by running processes | agent-builder, data-privacy, doc-converter, evals-judge, speech-to-text, web-crawler |

The qdrant vector database follows the `base` group. `qdrant.enabled` overrides the group when you set it: `true` deploys qdrant with `base` off, and `false` leaves it out with `base` on.

### How do the switches combine?

The chart decides whether a service runs in this order:

1. `services.<name>.enabled`, if you set it, wins.
2. Otherwise, `groups.<group>.enabled` applies. Set it for every group in your values.

For example, these values turn on the `runtime` group but leave out speech-to-text:

```yaml theme={"dark"}
groups:
  runtime:
    enabled: true
services:
  speech-to-text:
    enabled: false
```

### Which group combinations are valid?

Don't rely on the chart's group defaults. Set all four group flags in your values, using one of these combinations:

| To install | `core` | `base` | `devtime` | `runtime` |
| - | - | - | - | - |
| Core services only | `true` | `false` | `false` | `false` |
| Core, base, and devtime services | `true` | `true` | `true` | `false` |
| Core, base, and runtime services | `true` | `true` | `false` | `true` |
| All services | `true` | `true` | `true` | `true` |

The `devtime` and `runtime` services call the `base` services, so turn `base` on whenever either of them is on. The chart doesn't check this for you. qdrant is left out together with `base`.

### How do the umbrella flags map?

| `ai-platform` umbrella | `flowx-ai` chart |
| - | - |
| `devtime.enabled` | `groups.devtime.enabled` |
| `runtime.enabled` | `groups.runtime.enabled` |
| `runtime.<name>.enabled` | `services.<name>.enabled` |
| Base services, always on | `groups.base.enabled`, required when `devtime` or `runtime` is on, or `services.<name>.enabled` per service |

If your values still contain a top-level `devtime` or `runtime` key, the chart fails to render with the message `top-level devtime/runtime flags moved`. This is intentional: left in place, those keys would be ignored and the groups would keep their chart defaults.

***

## Convert your values

The `flowx-ai` chart reads one values tree. Most of your settings go under three top-level keys:

* `global`: settings shared by every service, such as domain, identity provider, datastore servers, registry, and ingress
* `groups`: turns service groups on and off. See [Service groups](#service-groups).
* `services.<name>`: the settings of one service

The bundled components keep their own top-level keys: `qdrant`, `spicedb-operator`, `spicedb-cluster`, and `dataSync`.

The chart validates the shape of your values. It fails to render if it finds a service key at the top level instead of under `services`, a top-level `image` or `podLabels` key, or a group that the chart does not declare.

The per-service charts and the `flowx-ai` chart use the same values structure for a service, so most keys inside a service keep their names. The work is mostly moving keys and removing duplicates.

### From the per-service charts

**1. Merge the global blocks**

Take the `global` block of each release's current values and merge them into one `global` block. Where two releases set the same key to different values, choose the value that is correct for the whole platform.

**2. Move each service's keys under services**

Move every other top-level key of a service's values file under `services.<name>`. For example, `resources` in `values-application-manager.yaml` becomes `services.application-manager.resources`:

```yaml theme={"dark"}
# Before: values-application-manager.yaml
replicaCount: 2
resources:
  limits:
    memory: 2Gi

# After: one values file for the flowx-ai release
services:
  application-manager:
    replicaCount: 2
    resources:
      limits:
        memory: 2Gi
```

**3. Remove image tag overrides**

Delete `image.tag` from each service's values. The service version moves to `services.<name>.version`, which the chart pins to the platform release of the chart version. A leftover tag override keeps that service on its old version while the rest of the platform moves.

### From the ai-platform umbrella chart

**1. Move each service key under services**

Move each root-level service key under `services`. For example, `embedder:` becomes `services.embedder:`.

**2. Replace the devtime and runtime flags**

Use the mapping in [How do the umbrella flags map?](#how-do-the-umbrella-flags-map).

**3. Merge the global block**

Merge the umbrella's `global` block into the shared `global` block.

### Check the global settings

In the old layout, a `global` value reached only the services of its own release. In the `flowx-ai` chart, a `global` value reaches every service. Review these cases before you render:

* **Ingress or Gateway API.** The chart routes traffic through Gateway API by default, and `global.flowx.ingress.enabled` defaults to `false`. To keep Kubernetes Ingress, set `global.flowx.ingress.enabled: true`, and set `global.flowx.gateway.enabled: false` if the cluster has no Gateway API CRDs: turning Ingress on does not turn the Gateway API routes off. A service then gets an Ingress only when its own `ingress.enabled` is also on. Ingress mode does not strip path prefixes, so services served under a prefix, such as `/license`, `/ai`, and `/integration`, need regex paths with `rewrite-target` annotations. With Ingress on, five AI services also get an admin Ingress by default: ai-gateway, doc-converter, data-privacy, speech-to-text, and web-crawler. Set `ingress.admin.enabled: false` on any of them that has none today.
* **`global.env`, `global.podAnnotations`, and `global.replicaCount`.** If these came from your core releases, they now also apply to the AI services, for example a Java agent annotation on Python pods. Move them onto the core services that need them.
* **Values set to `null` to unset a default.** The chart validates values against one schema for all services and rejects some of these. Delete them: the result is the same when the chart sets no default for that key.

### Choose the release name

The release name becomes part of the name of the CA certificates init container. It is also passed to doc-parser, doc-converter, evals-judge, data-privacy, speech-to-text, and web-crawler in the `GRPC_HOST_RESOLVER_HELM_CHART` environment variable, for service discovery.

Choose a name that no other release in the namespace uses, and keep it for the life of the installation. A name that contains `flowx-ai`, such as `flowx-ai`, keeps the generated names short, because the chart does not append `-flowx-ai` to it.

### Render and compare

Render the chart with your new values before the maintenance window, and compare the result with what runs today. Use the chart reference from your FlowX chart registry, and the platform release as the chart version:

```bash theme={"dark"}
helm template {your-release-name} {flowx-ai-chart-reference} --version {chart-version} \
  -n "$NS" -f values-flowx-ai.yaml > rendered.yaml

# Environment variables per Deployment, today
kubectl -n "$NS" get deploy -o json | jq -r '.items[] | .metadata.name as $n
  | .spec.template.spec.containers[0].env[]? | "\($n)\t\(.name)=\(.value // "")"' \
  | sort > env-live.txt

# Environment variables per Deployment, after the migration
yq -o json 'select(.kind == "Deployment")' rendered.yaml | jq -rs '.[] | .metadata.name as $n
  | .spec.template.spec.containers[0].env[]? | "\($n)\t\(.name)=\(.value // "")"' \
  | sort > env-new.txt

diff env-live.txt env-new.txt
```

Every line in the diff is a behavior change. Resolve each one, or confirm that you expect it, before you continue. Pay particular attention to variables that contain `BUCKET`, `URL`, and `REALM`.

The comparison covers inline `env` values of the first container only. Compare variables that come from `valueFrom` or `envFrom` (Secrets and ConfigMaps), and any other containers, separately.

***

## Migrate S3 buckets

The `flowx-ai` chart sets no bucket names and no bucket prefixes. Carry over the names your services use today, or they start writing to new, empty buckets.

> [!WARNING]
>
> A wrong bucket name does not always fail loudly. A service that is allowed to create buckets creates a missing bucket on first use. New files go to the new bucket, and files stored under the old name are no longer found. Nothing is deleted, but uploaded documents, media library assets, and knowledge base files appear to be missing.

### How do bucket names reach a service?

If a value is set, the chart renders it as an environment variable, such as `APPLICATION_FILE_STORAGE_S3_BUCKET_PREFIX` as rendered by the chart. If it is not set, the chart renders nothing, and the service falls back to the default built into its image. Several of those image defaults are empty.

A service that uses a prefix derives its bucket names from it: `<prefix>-<file type>` for regular uploads, and `<prefix>` alone for date-partitioned files. Dots become dashes.

### Bucket keys and previous defaults

The following table lists every bucket setting and the name a previous chart set when you did not.

| Service | Key in `flowx-ai` values | Per-service chart default | `ai-platform` umbrella default |
| - | - | - | - |
| application-manager | `services.application-manager.component.s3.main.bucketPrefix` | None (image default) | Not in this chart |
| runtime-manager | `services.runtime-manager.component.s3.main.bucketPrefix` | None (image default) | Not in this chart |
| integration-designer | `services.integration-designer.component.s3.main.bucketPrefix` | None (image default) | Not in this chart |
| document-plugin | `services.document-plugin.component.s3.main.bucketPrefix` | `flowx-dev` | Not in this chart |
| notification-plugin | `services.notification-plugin.component.s3.main.bucketPrefix` | `flowx-dev` | Not in this chart |
| cms-core, public | `services.cms-core.component.s3.public.bucketName` and `.rootDirectory` | `dev-env-media-library`, root directory `pr` | Not in this chart |
| cms-core, private | `services.cms-core.component.s3.private.bucketName` and `.rootDirectory` | `cms-private-bucket`, root directory `devtest` | Not in this chart |
| agent-builder | `services.agent-builder.component.s3.workflows.bucketName` | `workflows-bucket` | `workflows-bucket` |
| doc-parser | `services.doc-parser.component.s3.documents.bucketName` | `email-ingest` | `email-ingest` |
| kb-enrichment | `services.kb-enrichment.component.s3.applicationFileStorage.bucketName` | `kb-enrichment-bucket` | `kb-enrichment-bucket` |
| knowledgebase-indexer-v2 | `services.knowledgebase-indexer-v2.component.s3.applicationFileStorage.bucketName` | `kb-enrichment-bucket` (shared with kb-enrichment) | `kb-enrichment-bucket` |
| data-privacy | `services.data-privacy.component.s3.documents.bucketName` | `data-privacy-bucket` | `data-privacy-bucket` |

What this means for your previous charts:

* **From the per-service charts:** document-plugin, notification-plugin, cms-core, and the AI services in the table had bucket names set by their chart. The `flowx-ai` chart does not set them, so set them yourself, even if you never changed them.
* **From the `ai-platform` umbrella chart:** set the AI bucket names explicitly. Most AI image defaults are empty.
* **Services marked None:** they used their image default, and keep it if you leave the key unset.

### Step 1: Find the names in use

List the bucket and root directory variables that each running service uses:

```bash theme={"dark"}
kubectl -n "$NS" get deploy -o json | jq -r '.items[] | .metadata.name as $n
  | .spec.template.spec.containers[0].env[]?
  | select(.name | test("BUCKET|ROOT"))
  | "\($n)\t\(.name)=\(.value)"'
```

The filter is broad on purpose, so it can also list unrelated variables, such as a root log level. A service that is missing from the output uses its image default. In your object store, confirm which buckets hold the data, for example with the MinIO console, `mc ls`, or `aws s3 ls`.

### Step 2: Set the names in your values

Set every name from step 1 under the keys in the table:

```yaml theme={"dark"}
services:
  application-manager:
    component:
      s3:
        main:
          bucketPrefix: "{your-bucket-prefix}"
  cms-core:
    component:
      s3:
        public:
          bucketName: "{your-public-media-bucket}"
          rootDirectory: "{your-public-root-directory}"
        private:
          bucketName: "{your-private-media-bucket}"
          rootDirectory: "{your-private-root-directory}"
  doc-parser:
    component:
      s3:
        documents:
          bucketName: "{your-documents-bucket}"
```

Leave a key unset only for a service that should keep its image default. If an earlier values file sets a name you want to clear, remove the key from that file. Do not set it to `null`.

Two differences from the per-service charts need attention when you copy values across:

* **Entry name.** The per-service core charts named the S3 entry `minio`. In `flowx-ai` the entry is `main`. Set the prefix under `component.s3.main` and remove the `minio` entry: if both are present, the chart fails to render with `Only one s3 entry may omit an alias`.
* **cms-core root directory.** The per-service chart also set a `rootDirectory` for each cms-core bucket. Files are stored under that directory, so carry over `rootDirectory` together with `bucketName`.

If you set the document-plugin prefix with the older `MINIO_BUCKET_PREFIX` environment variable, move it to `bucketPrefix`. document-plugin reads `MINIO_BUCKET_PREFIX` only as a fallback, so a prefix that the chart renders takes precedence.

Before the maintenance window, check the names in the render:

```bash theme={"dark"}
yq -o json 'select(.kind == "Deployment")' rendered.yaml | jq -rs '.[] | .metadata.name as $n
  | .spec.template.spec.containers[0].env[]?
  | select(.name | test("BUCKET|ROOT"))
  | "\($n)\t\(.name)=\(.value)"'
```

Every name from step 1 must appear with the same value.

### Step 3: Verify the bucket names after the cutover

**1. Check the running values**

Run the command from step 1 again and check each value.

**2. Open files uploaded before the migration**

Open a process attachment, a media library asset, a notification template asset, and a knowledge base document.

**3. Check the logs**

Look for `bucket not found` errors in the service logs. They point to a name mismatch.

> [!WARNING]
>
> Do not rename buckets or change root directories as part of this migration. The databases reference stored files by bucket and path, so keep every bucket name, prefix, and `rootDirectory` exactly as it is today.

***

## PVC behavior

qdrant is the only workload in the chart that stores data on a PersistentVolumeClaim (PVC). Its volumes survive the cutover and are reattached to the new StatefulSet, as long as the conditions in this section hold.

If the `base` group is off and `qdrant.enabled` is not set, or if you set `qdrant.enabled: false`, the chart deploys no qdrant StatefulSet. Existing qdrant claims then stay in the namespace unused, until you delete them or turn qdrant back on.

### Volumes the chart uses

| PVC | Mounted by | Default size | Values key |
| - | - | - | - |
| `qdrant-storage-qdrant-0` | qdrant, collection data | `30Gi` | `qdrant.persistence` |
| `qdrant-snapshots-qdrant-0` | qdrant, snapshots | `30Gi` | `qdrant.snapshotPersistence` |

Databases, Kafka, and the object store run in their own releases. The chart does not create, change, or delete their volumes.

### Why do the volumes survive?

* **PVC names do not change.** A StatefulSet names each claim `<volumeClaimTemplate>-<StatefulSet name>-<ordinal>`. The chart names the StatefulSet `qdrant` through `qdrant.fullnameOverride`, so the new StatefulSet requests the same claims and binds the existing volumes.
* **Deleting the StatefulSet keeps its PVCs.** Kubernetes applies the StatefulSet's `persistentVolumeClaimRetentionPolicy`. The qdrant chart does not set it, so the default `whenDeleted: Retain` applies. The StorageClass `reclaimPolicy` plays no part, because the PVCs are never deleted and the volumes are never released.
* **`helm uninstall` keeps them too.** Helm deletes only the objects in the release manifest, and PVCs that a StatefulSet creates from its templates are not in the manifest.

> [!WARNING]
>
> Do not delete the qdrant StatefulSet with `--cascade=orphan`. That option keeps the old pod running, but the new StatefulSet has a different selector and cannot adopt it, so it cannot start its own pod either.

### Before the cutover: check the volumes

**1. Confirm the claim names**

```bash theme={"dark"}
kubectl -n "$NS" get pvc | grep qdrant
```

Expect `qdrant-storage-qdrant-0` and, if snapshots were on, `qdrant-snapshots-qdrant-0`.

**2. Check the retention policy**

```bash theme={"dark"}
kubectl -n "$NS" get statefulset -o custom-columns=NAME:.metadata.name,WHEN_DELETED:.spec.persistentVolumeClaimRetentionPolicy.whenDeleted
```

An empty value or `Retain` is safe. If the value is `Delete`, the PVCs are deleted together with the StatefulSet. Change the policy to `Retain` before the cutover:

```bash theme={"dark"}
kubectl -n "$NS" patch statefulset qdrant --type merge \
  -p '{"spec":{"persistentVolumeClaimRetentionPolicy":{"whenDeleted":"Retain"}}}'
```

**3. Compare sizes and StorageClass**

Compare your existing claims with `qdrant.persistence` and `qdrant.snapshotPersistence`. The StatefulSet reuses an existing claim as is, whatever the template says, but matching values keep the release definition accurate.

**4. Decide on a snapshots claim**

If your installation never had a snapshots claim, decide whether you want one. With `qdrant.snapshotPersistence.enabled: true`, the default, the chart creates a new `30Gi` claim. Set it to `false` to keep the current layout.

### What if your claim names are different?

If the first check shows other names, for example because the qdrant StatefulSet carried a release prefix, the new StatefulSet does not find your volumes. It creates new, empty claims instead, and the AI services lose their indexed collections.

Before the cutover, do one of the following, and test it in a non-production environment first:

* Set `qdrant.fullnameOverride` to the existing StatefulSet name, and set `global.flowx.qdrant.servers.flowx.grpc.host` to that name's headless Service, `{existing-name}-headless`, so that both the claim names and the service address match. The chart default is `qdrant-headless`.
* Take a qdrant snapshot of each collection, migrate with new claims, and restore the snapshots afterwards.

### After the cutover

Confirm that the new pod uses the existing claims and that no new claim appeared:

```bash theme={"dark"}
kubectl -n "$NS" get pvc | grep qdrant
kubectl -n "$NS" get pod qdrant-0 -o jsonpath='{.spec.volumes[*].persistentVolumeClaim.claimName}'
```

The `AGE` column of each claim must show its original age.

***

## Other behavior changes

Review these changes against your installation before the cutover. Most need no action.

| Area | Change | What to do |
| - | - | - |
| **Labels** | `app.kubernetes.io/name` is `flowx-ai` on every platform service object, and the service name moves to `app.kubernetes.io/component`. `app.kubernetes.io/version` is no longer set on those objects. The bundled qdrant keeps its `app.kubernetes.io/name: qdrant` and `app.kubernetes.io/version` labels. | Update dashboards, alerts, network policies, and scripts that select on the old labels. |
| **Renamed objects** | The release ConfigMap is `flowx-ai-release` instead of `flowx-release`. The data-sync Job is named `data-sync-<chart-version>`, with dots in the version replaced by dashes, such as `data-sync-5-9-6`. | Delete the old `flowx-release` ConfigMap and data-sync Job after the cutover, if your deploy tool does not prune them. |
| **Web app rename** | The `react-saas` web app is renamed `solution-portal`, so its objects get the new name. | Remove the old `react-saas` workload and its `react-saas-public` Ingress with the other old workloads, before the first install. While the old Ingress holds the host, the ingress controller can refuse the renamed Ingress. |
| **SpiceDB** | The chart installs the SpiceDB operator and a SpiceDB cluster by default. | If SpiceDB already runs in your environment, set `spicedb-operator.enabled: false` and `spicedb-cluster.enabled: false`. Never run two SpiceDB clusters on one datastore. To move it into the chart later, see [Move an existing SpiceDB into the chart](#move-an-existing-spicedb-into-the-chart). |
| **Identity provider clients** | events-gateway authenticates with its own client, `flowx-events-gateway-sa`. | Make sure the client and its secret exist in your realm. Without it, real-time updates fail with `403`. |
| **Identity provider URL** | Services use one identity provider URL from `global.flowx.idp`. FlowX Designer and the web app take their public URL from their own client entry. | If you point the global URL at an in-cluster address, set the public URL on the `designer` and `solution-portal` clients. Otherwise make sure pods can reach the public URL. |
| **Ingress routing** | In Ingress mode the chart does not strip path prefixes. Gateway API mode does, through `stripPrefix`. | In Ingress mode, add regex paths with `rewrite-target` annotations for services served under a prefix, such as `/license`, `/ai`, and `/integration`. Or use Gateway API mode. |
| **Ingress TLS** | An Ingress gets TLS when `tls` is `"true"`, which names the secret `<host>-tls`, or when `ingress.<type>.extraTls` is set. `global.flowx.ingress.<type>.tls` overrides the per-service `tls` value. | `<host>-tls` suits cert-manager. With a wildcard certificate, set `extraTls` on each service with your secret name. |
| **Image pull secrets** | The chart applies the global image pull secrets to every pod, AI services included. | Create the pull secrets in the namespace and list them in your `global` values. |
| **Probes** | Probe timings are set per service by the chart, and some AI services have no probes. | If you tuned probes in your previous values, compare `livenessProbe`, `readinessProbe`, and `startupProbe` in `rendered.yaml` with your running Deployments. |
| **CA certificates init container** | Services that import a certificate run a CA certificates init container that requests `100m` CPU and `128Mi` memory. By default these are admin, audit-core, data-search, document-plugin, and process-engine, which import the Elasticsearch CA. | Check namespace quotas if they are tight. |
| **Document storage** | The chart sets the file partition strategy of document-plugin and integration-designer to `PROCESS_DATE`, so new files are stored in date-partitioned paths. The per-service charts did not set it, and the service default is `NONE`. Existing files stay readable: each service stores a file's full path when it saves the file and reads it back from that path. With `PROCESS_DATE`, document-plugin rejects Kafka document requests that have no `processStartDate` header and sends an error to the reply topic. The process engine sends this header. | If you migrate from the per-service charts, expect `APPLICATION_FILESTORAGE_PARTITIONSTRATEGY=PROCESS_DATE`, as rendered by the chart, in the render diff for these two services. If your own apps publish directly to the document-plugin topics, make sure they send the `processStartDate` header. To keep the previous layout, set `APPLICATION_FILESTORAGE_PARTITIONSTRATEGY: "NONE"` under `services.document-plugin.env` and `services.integration-designer.env`. |

***

## Run the cutover

The cutover replaces the old releases with the `flowx-ai` release. How you carry it out depends on your deployment tooling: this section describes what the cutover must achieve.

Complete [Convert your values](#convert-your-values), [Migrate S3 buckets](#migrate-s3-buckets), the checks in [PVC behavior](#pvc-behavior), and the review of [Other behavior changes](#other-behavior-changes) first.

### Prepare

**1. Freeze the namespace**

Freeze all other changes to the namespace, so that the cutover changes only the charts.

**2. Take the backups**

Take the backups listed in [Prerequisites](#prerequisites).

**3. Keep the old definitions**

Keep a copy of the old releases' values and rendered manifests. You need them to [roll back](#roll-back).

**4. Rehearse**

Measure how long the cutover takes in a test environment, and size the maintenance window from it.

### What must the cutover do?

**1. Stop the old releases from managing their workloads**

Make sure nothing recreates the old Deployments or StatefulSet while you replace them, for example an automated sync or a scheduled pipeline.

**2. Remove the old Deployments and the qdrant StatefulSet**

Their selectors cannot change, so they must be deleted before the new release can create its own. **The outage starts here.**

* Remove only the platform workloads. If your databases, Kafka, or object store run in the same namespace, make sure their StatefulSets are not caught by the same operation.
* Delete the qdrant StatefulSet with normal cascading deletion, not as an orphan. See [PVC behavior](#pvc-behavior). If the `flowx-ai` release keeps the release name of your `ai-platform` release, the qdrant selector does not change and the StatefulSet can stay.

**3. Hand over or remove the other objects of the old releases**

Services, Ingresses, ServiceAccounts, ConfigMaps, and RBAC objects keep their names in `flowx-ai`, but Helm refuses to install an object that another release owns. Either remove them with the old releases, or transfer their ownership to the new release.

Helm reads ownership from the annotations `meta.helm.sh/release-name` and `meta.helm.sh/release-namespace` and the label `app.kubernetes.io/managed-by: Helm`. To hand an object over, set the two annotations to the `flowx-ai` release name and namespace.

**4. Preserve what the new release does not create**

If the old releases created Secrets or ConfigMaps through their values that your services still need, recreate them outside the old releases, or make sure your `flowx-ai` values create them.

**5. Install the flowx-ai release**

Install it with your new values. The outage ends when every Deployment is available and the qdrant pod has reattached its volumes.

**6. Remove objects that changed name**

The release ConfigMap `flowx-release` is replaced by `flowx-ai-release`, and the data-sync Job gets a new name that includes the chart version. Jobs are immutable, so the old data-sync Job is not updated in place. After the cutover, delete the old objects if your tooling does not prune them.

> [!TIP]
>
> With plain Helm, the simplest cutover is: recreate outside Helm any Secrets the old releases created, run `helm uninstall` for each old release (the qdrant claims are kept), then run `helm install {your-release-name} {flowx-ai-chart-reference} --version {chart-version} -n "$NS" -f values-flowx-ai.yaml`.

> [!NOTE]
>
> Do not leave a force-replace or recreate option turned on permanently in your tooling to get through the removal step. It would delete and recreate workloads on every later deployment, not only during the cutover.

***

## Verify the migration

Run these checks right after the cutover, and again after the first day of normal use:

* Every Deployment is available, and only the services of your enabled groups run: `kubectl -n "$NS" get deploy`.
* No workload from an old release remains: `kubectl -n "$NS" get deploy,statefulset -L app.kubernetes.io/name,app.kubernetes.io/instance` shows `flowx-ai` on every platform workload and `qdrant` on the qdrant StatefulSet, all with your release name as the instance.
* The qdrant pod uses the original claims. See [After the cutover](#after-the-cutover).
* The bucket variables match the names you set. See [Step 3: Verify the bucket names after the cutover](#step-3-verify-the-bucket-names-after-the-cutover).
* You can sign in to FlowX Designer and to a deployed app.
* A process instance starts, and its UI updates in real time. This exercises events-gateway.
* A file uploaded before the migration opens, and a new upload succeeds.
* If AI groups are on, an AI feature answers, for example a chat through ai-gateway or a knowledge base query.
* Scheduled jobs run at their next trigger.
* The service logs show no `bucket not found`, `401`, `403`, or connection errors that were not there before.
* Your monitoring still finds the services. Dashboards, alerts, and scripts that select on `app.kubernetes.io/name=<service>` must now select on `app.kubernetes.io/component=<service>`.

***

## Roll back

A rollback crosses the same selector change in the other direction, so it is also a delete and recreate. A Helm release history rollback cannot do it, because the old and new workloads belong to different releases.

The qdrant PVCs were never deleted, so the old releases reattach them. If the new services changed data that the old versions cannot read, restore the backup you took before the window.

**1. Remove the flowx-ai release**

Stop your tooling from managing the `flowx-ai` release, then run `helm uninstall` for it, or remove its workloads and the objects it owns. The qdrant claims are kept. If the release installed the SpiceDB operator and cluster, they are removed with it: make sure the old releases bring back the SpiceDB setup you ran before.

**2. Restore the datastores if needed**

Restore PostgreSQL, MongoDB, and the object store from the backup.

**3. Reinstall the old releases**

Use their previous values, at the chart versions you ran before.

**4. Check qdrant**

Confirm that the qdrant pod reattached its claims. If you need to restore the qdrant collections, restore them from the snapshots now, while qdrant is running.

> [!NOTE]
>
> Files written while `flowx-ai` was running go to the buckets that its values named. If those names differed from the old ones, the old releases cannot see those files after a rollback. Carrying over the bucket names as described in [Migrate S3 buckets](#migrate-s3-buckets) avoids this. document-plugin and integration-designer also stored new files in date-partitioned paths. Test the rollback in a non-production environment before you rely on it.

Keep the old values and manifests until you have signed off the migration.

***

## Move an existing SpiceDB into the chart

If SpiceDB runs as its own Helm releases or Argo CD Applications, with the SpiceDB operator and the SpiceDB cluster installed separately from FlowX.AI, you can move it into the operator and cluster that the `flowx-ai` chart bundles. Skip this section if the chart you migrated from already installed SpiceDB.

> [!WARNING]
>
> Move SpiceDB in its own maintenance window, after the cutover to the `flowx-ai` chart has run for a while. Every permission check fails while SpiceDB is down, so the move is a platform outage. Never combine it with the cutover.

### How the move keeps your data

SpiceDB keeps no state in its pods. Every relationship and the permission schema live in the PostgreSQL datastore that its `datastore_uri` points at. The move copies no data: the chart's SpiceDB cluster serves the same datastore that the old one did.

Before the operator starts any SpiceDB pods for a cluster, it runs a migration Job that brings the datastore to the SpiceDB version you set. At the version the datastore already runs, the Job changes nothing. Migrations only go forward, so move SpiceDB at the version you run today and upgrade it afterwards. That also keeps a rollback free of data loss.

| | Separate installation | `flowx-ai` chart |
| - | - | - |
| **Operator** | Its own release | Deployment in the FlowX.AI namespace, watching only that namespace |
| **Cluster** | Named after its release, often `spicedb` | `SpiceDBCluster` named `spicedb-cluster` |
| **Address the services use** | `{old-cluster-name}:50051` | `spicedb-cluster:50051`, from `global.flowx.spicedb.servers.flowx.host` |
| **CRD** | Installed by the old operator release | Shipped in the operator subchart. Helm installs it only when it's missing and never upgrades or deletes it |
| **Versions** | Whatever you pinned | SpiceDB v1.52.0 and operator v1.25.1. The operator can migrate a PostgreSQL datastore from any version between v1.2.0 and v1.52.0 |
| **Credentials** | A Secret with `preshared_key` and `datastore_uri` | The same keys, in a Secret named `spicedb` |

### Before the window

**1. Record what runs today**

You need these values for a rollback:

```bash theme={"dark"}
kubectl -n "$NS" get spicedbcluster \
  -o custom-columns=NAME:.metadata.name,VERSION:.status.version.name,MIGRATION:.status.migration
kubectl get deploy -A | grep -i spicedb-operator
mkdir -p rollback
kubectl -n "$NS" get spicedbcluster {old-cluster-name} -o yaml > rollback/old-spicedbcluster.yaml
helm -n "$NS" get values {old-operator-release} > rollback/old-operator-values.yaml
helm -n "$NS" get values {old-cluster-release} > rollback/old-cluster-values.yaml
```

**2. Choose the version for the move**

If `VERSION` is `v1.52.0`, there's nothing to set. If it's older, pin the chart's cluster to it:

```yaml theme={"dark"}
spicedb-cluster:
  version: {running-version}
```

A cluster newer than v1.52.0, or an operator newer than v1.25.1, can't move into this chart version.

**3. Protect the CRD**

Check whether the old operator release manages the `spicedbclusters.authzed.com` CRD as a regular resource:

```bash theme={"dark"}
helm -n "$NS" get manifest {old-operator-release} | grep -c CustomResourceDefinition
```

`0` means the CRD isn't part of the release's manifest, and uninstalling the release leaves it in place. Any other result means uninstalling the release deletes the CRD, and with it every `SpiceDBCluster` in the cluster. In that case, keep it before the window:

```bash theme={"dark"}
kubectl annotate crd spicedbclusters.authzed.com helm.sh/resource-policy=keep
```

An Argo CD Application that manages the CRD deletes it on a cascading delete.

**4. Prepare the Secret**

The chart reads a Secret named `spicedb` with the keys `preshared_key` and `datastore_uri`. If your Secret has another name, create `spicedb` with the same values. Don't rotate the preshared key in this window.

**5. Check the datastore user and the images**

The migration Job needs a datastore user that can create and alter tables and indexes. Make sure the namespace can pull the `spicedb` and `spicedb-operator` images from your registry.

**6. Rehearse the backup**

Back up the SpiceDB PostgreSQL database and restore it on a copy. That backup is your recovery point.

### Run the move

**1. Cut over with SpiceDB outside the chart**

Do this in an earlier window, as part of the cutover, with these values:

```yaml theme={"dark"}
spicedb-operator:
  enabled: false
spicedb-cluster:
  enabled: false
global:
  flowx:
    spicedb:
      servers:
        flowx:
          host: {old-cluster-name}
```

**2. Apply the chart's CRD**

Take `crds/authzed.com_spicedbclusters.yaml` from the `spicedb-operator` subchart in the `flowx-ai` chart and apply it. This is safe while the old operator runs:

```bash theme={"dark"}
kubectl apply --server-side -f authzed.com_spicedbclusters.yaml
```

**3. Back up the datastore**

Back up the SpiceDB PostgreSQL database now. A rollback restores this state.

**4. Delete the old cluster**

Delete it while the old operator still runs, so the operator removes its pods and Service. The outage starts here.

```bash theme={"dark"}
helm -n "$NS" uninstall {old-cluster-release}
kubectl -n "$NS" get spicedbcluster,deploy,svc | grep -i spicedb
```

The second command must return nothing from the old cluster.

**5. Remove the old operator and keep the CRD**

Uninstall the old operator release. If you installed the upstream operator bundle, delete its Deployment, ServiceAccount, roles, and bindings by name, and never delete the bundle file as a whole, because it contains the CRD. With Argo CD, delete the Application without cascading, then delete the Deployment and roles by hand.

```bash theme={"dark"}
kubectl get crd spicedbclusters.authzed.com
kubectl get deploy -A | grep -i spicedb-operator
```

The CRD must still exist, and no operator Deployment may remain.

**6. Turn on SpiceDB in the chart**

Upgrade the release with:

```yaml theme={"dark"}
spicedb-operator:
  enabled: true
spicedb-cluster:
  enabled: true
  version: {running-version}   # omit when it's v1.52.0
  replicas: {replicas-you-ran-before}
global:
  flowx:
    spicedb:
      servers:
        flowx:
          host: spicedb-cluster
```

**7. Wait for the migration Job**

Nothing else starts until the Job completes:

```bash theme={"dark"}
kubectl -n "$NS" get spicedbcluster spicedb-cluster -o yaml \
  | yq '.status | {"conditions": .conditions, "migration": .migration, "version": .version}'
kubectl -n "$NS" get jobs | grep spicedb-cluster
```

The move is through when the cluster no longer reports a migration in progress, `status.migration` matches what you recorded, and the `spicedb-cluster` Deployment exists and is ready. If the Job fails, read its log: a wrong `datastore_uri`, an `sslmode` mismatch, or a datastore user without rights to change tables are the usual causes. Fix the cause and let the operator run the Job again. Don't delete the cluster, and don't start the old cluster on the same datastore.

**8. Check the services**

Changing the host restarts the services that connect to SpiceDB. Check that every pod runs and that authorization-system logs no SpiceDB errors, then sign in to FlowX.AI Designer and do one action that needs a permission. The outage ends here.

Keep the `rollback` folder and the backup until you sign off. To upgrade SpiceDB to v1.52.0 afterwards, remove the `version` pin. The operator then runs a migration Job for each step, which you watch the same way.

### Roll back the move

If the migration Job can't pass in the window, or the services can't authorize after the move, roll back:

**1. Turn SpiceDB off in the chart again**

Apply the values from the first step of the move, with the host set back to `{old-cluster-name}`. The chart's cluster and operator are removed.

**2. Restore the datastore if the schema moved**

Compare `status.migration` from the migration Job step with the value you recorded before the window. If it changed, the old SpiceDB version can't start on the datastore: restore the backup you took in the window. Anything written after that backup is lost. If it didn't change, skip the restore.

**3. Reinstall the old operator and cluster**

Use the values in the `rollback` folder, at the versions you ran before. Wait for the old operator's migration Job, then check the services as in the last step of the move.

> [!WARNING]
>
> Never delete the `spicedbclusters.authzed.com` CRD, never run two operators that watch the same namespace or two SpiceDB clusters on one datastore, and never delete the migration Job, the cluster, or the operator while a migration runs.

***

## Troubleshooting

> [accordion] The render fails with top-level devtime/runtime flags moved
>
> Your values still contain the `devtime` or `runtime` key of the `ai-platform` umbrella chart. Replace them with `groups.devtime.enabled`, `groups.runtime.enabled`, and `services.<name>.enabled`, as described in [How do the umbrella flags map?](#how-do-the-umbrella-flags-map).

> [accordion] The render fails with Only one s3 entry may omit an alias
>
> A service has both the old `minio` S3 entry and the new `main` entry. Move the settings to `component.s3.main` and remove the `minio` entry.

> [accordion] The install fails with invalid ownership metadata
>
> An object from an old release still exists, and Helm will not take it over. Remove it, or hand it over by setting the `meta.helm.sh/release-name` and `meta.helm.sh/release-namespace` annotations to the `flowx-ai` release. See [What must the cutover do?](#what-must-the-cutover-do).

> [accordion] The upgrade fails with field is immutable
>
> You ran `helm upgrade` against an existing Deployment or StatefulSet whose selector the new chart changes. Delete the old workload first, as described in [What must the cutover do?](#what-must-the-cutover-do).

> [accordion] Files are missing, or the logs show bucket not found
>
> A bucket name or prefix did not carry over. Compare the values from [Step 1: Find the names in use](#step-1-find-the-names-in-use) with the running Deployments, set the missing names, and upgrade the release. Files stored under the old name are still in the old bucket.

> [accordion] qdrant starts with an empty collection set
>
> The new StatefulSet created new claims instead of reattaching yours. Check the claim names as described in [What if your claim names are different?](#what-if-your-claim-names-are-different), or restore the collections from your qdrant snapshots.

***

## Related resources

> [card] [AI Platform setup](https://docs.flowx.ai/6.x/setup-guides/ai-platform-setup.md)
>
> AI services, configuration, and deployment

> [card] [Qdrant setup](https://docs.flowx.ai/6.x/setup-guides/qdrant.md)
>
> Vector database configuration

> [card] [SpiceDB](https://docs.flowx.ai/6.x/setup-guides/spicedb.md)
>
> Authorization service setup

> [card] [5.9.6 release notes](https://docs.flowx.ai/release-notes/v5.x/v5.9.6-october-2026/v5.9.6-october-2026.md#upgrade-notes)
>
> Upgrade notes for FlowX.AI 5.9.6


## Related topics

- [Helm chart](https://docs.flowx.ai/release-notes/v6.x/v6.0.0-october-2026/migrating-from-v5.9-to-v6.0/helm-chart.md)
- [Configure the flowx-ai Helm chart for your infrastructure](https://docs.flowx.ai/6.x/setup-guides/configure-flowx-ai-helm-chart.md)
- [FlowX.AI 5.9.0 Release Notes](https://docs.flowx.ai/release-notes/v5.x/v5.9.0-june-2026/v5.9.0-june-2026.md)
- [Deployment guidelines v6.0.0](https://docs.flowx.ai/release-notes/v6.x/v6.0.0-october-2026/deployment-guidelines-v6.0.md)
- [FlowX.AI 5.9.4 Release Notes (LTS)](https://docs.flowx.ai/release-notes/v5.x/v5.9.4-august-2026/v5.9.4-august-2026.md)


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