Skip to content

Conversation

@jayblanc
Copy link
Contributor

Summary

This PR refactors how Apache Unomi selects and installs its startup feature set (formerly driven by the startFeatures OSGi configuration and the semantic overloading of UNOMI_AUTO_START). It introduces proper Karaf features to represent Unomi “distributions” (search backend variant + ancillary feature set) and restores UNOMI_AUTO_START to its original purpose (auto–start true/false). A new environment variable and/or setup mechanism is added to select the desired search backend distribution cleanly and persistently.

It also removes the HealthCheck delegator introduced earlier and reverts to the standard pattern of provider activation via configuration, now split per backend through dedicated healthcheck features.


Motivation

The previous approach:

  • Overloaded UNOMI_AUTO_START to include backend selection (e.g. elasticsearch | opensearch) which was confusing to new users.
  • Introduced a custom OSGi configuration (startFeatures) holding a comma–separated list of features, duplicating what Karaf feature descriptors already provide (dependency management, versioning, externalization).
  • Added a HealthCheck delegator layer to route to the correct search provider, while Unomi already supports selective provider activation via config.

This PR adopts standard Karaf feature mechanisms for clarity, maintainability, and easier custom packaging, while simplifying health checks.


Key Changes

  1. Distribution Feature Introduction

    • Added two Karaf features :
      • unomi-distribution-elasticsearch
      • unomi-distribution-opensearch
    • These features aggregate the previously listed startup features and include the appropriate healthcheck feature.
    • New Maven module distribution housing the built-in feature XML descriptors (included in the package).
    • Enables external (custom) distributions by publishing feature XMLs to Maven repos.
  2. Environment Variable / Setup Refactor

    • Restored UNOMI_AUTO_START to a boolean/legacy behavior (e.g. auto-start service on container launch).
    • Introduced UNOMI_DISTRIBUTION to specify which distribution feature to use (defaults to Elasticsearch if unset).
    • Docker image and startup scripts updated to translate UNOMI_DISTRIBUTION into an internal unomi:setup persisted value.
    • UnomiManagementService set the distribution to unomi-distribution-elasticsearch if not set in environment
    • Added unomi:setup <distributionFeatureName> command:
      • Persists chosen distribution (unless overridden with a force/overwrite flag).
      • Guards against accidental production backend switching.
  3. Removal of Custom startFeatures OSGi Config

    • Deleted the startFeatures OSGi config key and associated management logic from UnomiManagementService.
    • Simplified startup path: now just resolves the selected distribution feature and installs it by iterating on feature dependencies (as previously done with the startFeatures list).
  4. HealthCheck Simplification

    • Removed the Delegator pattern.
    • Reintroduced separate providers with activation governed by configuration.
    • Added two healthcheck feature descriptors (e.g. unomi-healthcheck-elasticsearch, unomi-healthcheck-opensearch).
    • Each healthcheck feature carries a config file enabling only the relevant providers.
    • Search backend providers now defensively check activation in init() to avoid unnecessary client construction when not active.
  5. Docker & Packaging Adaptations

    • Updated entrypoint / scripts to:
      • Respect UNOMI_DISTRIBUTION.
      • No longer misuse UNOMI_AUTO_START for backend selection.
    • Documentation/examples updated to show:
      • UNOMI_AUTO_START=true
      • UNOMI_DISTRIBUTION=unomi-distribution-opensearch (or elasticsearch).

Upgrade / Migration Notes

If you previously used Unomi Docker images with:

UNOMI_AUTO_START=elasticsearch

(or opensearch)

You must now use:

UNOMI_AUTO_START=true
UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch

(or unomi-distribution-opensearch).

If you previously started Unomi in Karaf shell with:

unomi:start elasticsearch

(or opensearch)

You must now use:

unomi:setup unomi-distribution-elasticsearch
unomi:start

(or unomi-distribution-opensearch).

Custom startup feature lists should be converted into a Karaf feature XML and published as a Maven artifact; then set UNOMI_DISTRIBUTION to that feature name.


Testing Performed

  • Unit tests updated for ManagementService (distribution resolution logic).
  • Integration tests for both distributions ensure feature installation completes and backend connects.
  • Healthcheck endpoints validated for each backend returning expected checks.
  • Docker image smoke-tested with both UNOMI_DISTRIBUTION values.
  • Negative test: unspecified UNOMI_DISTRIBUTION defaults to elasticsearch.
  • Re-run after switching distribution (if setup command supports overwrite).

Related JIRA / PR Links

  • JIRA: UNOMI-919
  • Prior PR (context – OpenSearch integration): #715

@jayblanc jayblanc marked this pull request as ready for review December 9, 2025 12:33
diff --git c/bom/artifacts/pom.xml i/bom/artifacts/pom.xml
index b0c0ec5b2..ecc78948d 100644
--- c/bom/artifacts/pom.xml
+++ i/bom/artifacts/pom.xml
@@ -60,6 +60,21 @@
                 <artifactId>unomi-persistence-elasticsearch-core</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.unomi</groupId>
+                <artifactId>unomi-persistence-elasticsearch-conditions</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.unomi</groupId>
+                <artifactId>unomi-persistence-opensearch-core</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.unomi</groupId>
+                <artifactId>unomi-persistence-opensearch-conditions</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.unomi</groupId>
                 <artifactId>unomi-json-schema-services</artifactId>
@@ -243,6 +258,13 @@
                 <classifier>features</classifier>
                 <type>xml</type>
             </dependency>
+            <dependency>
+                <groupId>org.apache.unomi</groupId>
+                <artifactId>unomi-distribution</artifactId>
+                <version>${project.version}</version>
+                <classifier>features</classifier>
+                <type>xml</type>
+            </dependency>
         </dependencies>
     </dependencyManagement>

diff --git c/distribution/pom.xml i/distribution/pom.xml
new file mode 100644
index 000000000..0c469361a
--- /dev/null
+++ i/distribution/pom.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.unomi</groupId>
+        <artifactId>unomi-root</artifactId>
+        <version>3.1.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>unomi-distribution</artifactId>
+    <name>Apache Unomi :: Distribution</name>
+    <description>Apache Unomi Distribution's Karaf features assemblies for the Apache Unomi Context Server</description>
+    <packaging>kar</packaging>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.unomi</groupId>
+                <artifactId>unomi-bom</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-wab</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-metrics</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-services</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-scripting</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-elasticsearch-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-elasticsearch-conditions</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-opensearch-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-persistence-opensearch-conditions</artifactId>
+        </dependency>
+
+        <!-- plugins -->
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-plugins-base</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-plugins-request</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-plugins-mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-plugins-optimization-test</artifactId>
+        </dependency>
+
+        <!-- extensions -->
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-lists-extension-services</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-lists-extension-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-lists-extension-actions</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-geonames-services</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-geonames-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-privacy-extension-services</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>cxs-privacy-extension-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-json-schema-services</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-json-schema-rest</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>shell-dev-commands</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-web-tracker-wab</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+        <!-- Apache HTTP Client -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore-osgi</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient-osgi</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.karaf.tooling</groupId>
+                    <artifactId>karaf-maven-plugin</artifactId>
+                    <extensions>true</extensions>
+                    <configuration>
+                        <includeTransitiveDependency>false</includeTransitiveDependency>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-maven-plugin</artifactId>
+                <configuration>
+                    <startLevel>85</startLevel>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>generate-features</id>
+                        <phase>generate-resources</phase>
+                        <goals>
+                            <goal>features-generate-descriptor</goal>
+                        </goals>
+                        <configuration>
+                            <enableGeneration>true</enableGeneration>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>verify</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>mvn:org.apache.karaf.features/standard/${karaf.version}/xml/features</descriptor>
+                                <descriptor>mvn:org.apache.karaf.features/enterprise/${karaf.version}/xml/features</descriptor>
+                                <descriptor>file:${project.build.directory}/feature/feature.xml</descriptor>
+                            </descriptors>
+                            <distribution>org.apache.karaf:apache-karaf:zip:${karaf.version}</distribution>
+                            <javase>17</javase>
+                            <framework>
+                                <feature>framework</feature>
+                            </framework>
+
+                            <features>
+                                <feature>unomi-distribution-opensearch</feature>
+                                <feature>unomi-distribution-elasticsearch</feature>
+                            </features>
+
+                            <ignoreMissingConditions>false</ignoreMissingConditions>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git c/docker/README.md i/docker/README.md
index 83cce28a2..ae1e4f93b 100644
--- c/docker/README.md
+++ i/docker/README.md
@@ -45,55 +45,75 @@ If you want to run it without docker-compose you should then make sure you setup

 For ElasticSearch:

-    docker pull docker.elastic.co/elasticsearch/elasticsearch:7.4.2
-    docker network create unomi
-    docker run --name elasticsearch --net unomi -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e cluster.name=contextElasticSearch docker.elastic.co/elasticsearch/elasticsearch:7.4.2
+```bash
+docker pull docker.elastic.co/elasticsearch/elasticsearch:9.2.1
+docker network create unomi
+docker run -d --name elasticsearch --net unomi -p 9200:9200 -p 9300:9300 \
+    -e "discovery.type=single-node" \
+    -e "xpack.security.enabled=false" \
+    -e cluster.name=contextElasticSearch \
+    docker.elastic.co/elasticsearch/elasticsearch:9.2.1
+```

 For OpenSearch:

-    docker pull opensearchproject/opensearch:3.0.0
-    docker network create unomi
-    export OPENSEARCH_ADMIN_PASSWORD=enter_your_custom_admin_password_here
-    docker run --name opensearch --net unomi -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} opensearchproject/opensearch:3.0.0
+```bash
+docker pull opensearchproject/opensearch:3.0.0
+docker network create unomi
+export OPENSEARCH_ADMIN_PASSWORD=enter_your_custom_admin_password_here
+docker run -d --name opensearch --net unomi -p 9200:9200 -p 9300:9300 \
+    -e "discovery.type=single-node" \
+    -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
+    opensearchproject/opensearch:3.0.0
+```

 For Unomi (with ElasticSearch):

-    docker pull apache/unomi:3.0.0-SNAPSHOT
-    docker run --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-        -e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 \
-        apache/unomi:3.0.0-SNAPSHOT
+```bash
+docker pull apache/unomi:3.1.0-SNAPSHOT
+docker run -d --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
+    -e UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200 \
+    apache/unomi:3.1.0-SNAPSHOT
+```

 For Unomi (with OpenSearch):

-    docker pull apache/unomi:3.0.0-SNAPSHOT
-    docker run --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-        -e UNOMI_AUTO_START=opensearch \
-        -e UNOMI_OPENSEARCH_ADDRESSES=opensearch:9200 \
-        -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
-        apache/unomi:3.0.0-SNAPSHOT
+```bash
+docker pull apache/unomi:3.1.0-SNAPSHOT
+docker run -d --name unomi --net unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
+    -e UNOMI_DISTRIBUTION=unomi-distribution-opensearch \
+    -e UNOMI_OPENSEARCH_ADDRESSES=opensearch:9200 \
+    -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD}
+    apache/unomi:3.1.0-SNAPSHOT
+```

 ## Using a host OS Search Engine installation (only supported on macOS & Windows)

 For ElasticSearch:

-    docker run --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-        -e UNOMI_ELASTICSEARCH_ADDRESSES=host.docker.internal:9200 \
-        apache/unomi:3.0.0-SNAPSHOT
+```bash
+docker run -d --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
+    -e UNOMI_ELASTICSEARCH_ADDRESSES=host.docker.internal:9200 \
+    apache/unomi:3.1.0-SNAPSHOT
+```

 For OpenSearch:

-    docker run --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
-        -e UNOMI_AUTO_START=opensearch \
-        -e UNOMI_OPENSEARCH_ADDRESSES=host.docker.internal:9200 \
-        -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
-        apache/unomi:3.0.0-SNAPSHOT
+```bash
+docker run -d --name unomi -p 8181:8181 -p 9443:9443 -p 8102:8102 \
+    -e UNOMI_DISTRIBUTION=unomi-distribution-opensearch
+    -e UNOMI_OPENSEARCH_ADDRESSES=host.docker.internal:9200 \
+    -e UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_ADMIN_PASSWORD} \
+    apache/unomi:3.1.0-SNAPSHOT
+```

 Note: Linux doesn't support the host.docker.internal DNS lookup method yet, it should be available in an upcoming version of Docker. See https://github.com/docker/for-linux/issues/264

 ## Environment Variables

 ### Common Variables
-- `UNOMI_AUTO_START`: Specifies the search engine type (`elasticsearch` or `opensearch`, defaults to `elasticsearch`)
+- `UNOMI_AUTO_START`: Boolean to specify if unomi auto start with karaf (defaults to `true`)
+- `UNOMI_DISTRIBUTION`: Specifies the Unomi Distribution Feature to use (`unomi-distribution-elasticsearch` or `unomi-distribution-opensearch`, defaults to `unomi-distribution-elasticsearch`)

 ### ElasticSearch-specific Variables
 - `UNOMI_ELASTICSEARCH_ADDRESSES`: ElasticSearch host:port (default: localhost:9200)
diff --git c/docker/src/main/docker/docker-compose-build-es.yml i/docker/src/main/docker/docker-compose-build-es.yml
index af71f7717..454e10364 100644
--- c/docker/src/main/docker/docker-compose-build-es.yml
+++ i/docker/src/main/docker/docker-compose-build-es.yml
@@ -17,7 +17,7 @@
 version: '2.4'
 services:
   elasticsearch:
-    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
+    image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1
     volumes:
       - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data
     environment:
@@ -35,7 +35,8 @@ services:
     build: .
     image: apache/unomi:${project.version}
     environment:
-      - UNOMI_AUTO_START=elasticsearch
+      - UNOMI_AUTO_START=true
+      - UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch
       - UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
       # Debug settings
       - KARAF_DEBUG=${DEBUG:-false}
diff --git c/docker/src/main/docker/docker-compose-build-os.yml i/docker/src/main/docker/docker-compose-build-os.yml
index 0f736e004..fb2d27d27 100644
--- c/docker/src/main/docker/docker-compose-build-os.yml
+++ i/docker/src/main/docker/docker-compose-build-os.yml
@@ -95,7 +95,8 @@ services:
     image: apache/unomi:${project.version}
     container_name: unomi
     environment:
-      - UNOMI_AUTO_START=opensearch
+      - UNOMI_AUTO_START=true
+      - UNOMI_DISTRIBUTION=unomi-distribution-opensearch
       - UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200
       - UNOMI_OPENSEARCH_USERNAME=admin
       - UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
diff --git c/docker/src/main/docker/docker-compose-es.yml i/docker/src/main/docker/docker-compose-es.yml
index 28e800b4b..43f3bbba5 100644
--- c/docker/src/main/docker/docker-compose-es.yml
+++ i/docker/src/main/docker/docker-compose-es.yml
@@ -23,7 +23,7 @@ networks:

 services:
   elasticsearch:
-    image: docker.elastic.co/elasticsearch/elasticsearch:9.1.3
+    image: docker.elastic.co/elasticsearch/elasticsearch:9.2.1
     container_name: elasticsearch
     volumes:
       - unomi-3-elasticsearch-data:/usr/share/elasticsearch/data
@@ -41,7 +41,8 @@ services:
   node-1:
     image: apache/unomi:${project.version}
     environment:
-      - UNOMI_AUTO_START=elasticsearch
+      - UNOMI_AUTO_START=true
+      - UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch
       - UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
       # Debug settings
       - KARAF_DEBUG=${DEBUG:-false}
diff --git c/docker/src/main/docker/docker-compose-os.yml i/docker/src/main/docker/docker-compose-os.yml
index cf229dedf..03abe5263 100644
--- c/docker/src/main/docker/docker-compose-os.yml
+++ i/docker/src/main/docker/docker-compose-os.yml
@@ -79,7 +79,8 @@ services:
     image: apache/unomi:${project.version}
     container_name: unomi
     environment:
-      - UNOMI_AUTO_START=opensearch
+      - UNOMI_AUTO_START=true
+      - UNOMI_DISTRIBUTION=unomi-distribution-opensearch
       - UNOMI_OPENSEARCH_ADDRESSES=opensearch-node1:9200
       - UNOMI_OPENSEARCH_USERNAME=admin
       - UNOMI_OPENSEARCH_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD}
diff --git c/docker/src/main/docker/entrypoint.sh i/docker/src/main/docker/entrypoint.sh
index 275f0edb0..1cfc68456 100755
--- c/docker/src/main/docker/entrypoint.sh
+++ i/docker/src/main/docker/entrypoint.sh
@@ -29,14 +29,9 @@ if [ "$KARAF_DEBUG" = "true" ]; then
     export KARAF_DEBUG=true
 fi

-# Determine search engine type from UNOMI_AUTO_START
-SEARCH_ENGINE="${UNOMI_AUTO_START:-elasticsearch}"
-export KARAF_OPTS="-Dunomi.autoStart=${UNOMI_AUTO_START}"
+UNOMI_DISTRIBUTION="${UNOMI_DISTRIBUTION:-unomi-distribution-elasticsearch}"
+export KARAF_OPTS="-Dunomi.autoStart=${UNOMI_AUTO_START} -Dunomi.distribution=${UNOMI_DISTRIBUTION}"

-if [ "$SEARCH_ENGINE" = "true" ]; then
-    SEARCH_ENGINE="elasticsearch"
-fi
-echo "SEARCH_ENGINE: $SEARCH_ENGINE"
 echo "KARAF_OPTS: $KARAF_OPTS"

 # Function to check cluster health for a specific node
@@ -51,9 +46,11 @@ check_node_health() {
     fi
 }

-# Configure connection parameters based on search engine type
-if [ "$SEARCH_ENGINE" = "opensearch" ]; then
+# Configure connection parameters based on distribution name
+WAIT_SEARCH_ENGINE=true
+if [[ "$UNOMI_DISTRIBUTION" == *opensearch* ]]; then
     # OpenSearch configuration
+    SEARCH_ENGINE="opensearch"
     if [ -z "$UNOMI_OPENSEARCH_PASSWORD" ]; then
         echo "Error: UNOMI_OPENSEARCH_PASSWORD must be set when using OpenSearch"
         exit 1
@@ -65,8 +62,9 @@ if [ "$SEARCH_ENGINE" = "opensearch" ]; then
     curl_opts="-k -H \"${auth_header}\" -H \"Content-Type: application/json\""
     # Build array of node URLs
     IFS=',' read -ra NODES <<< "${UNOMI_OPENSEARCH_ADDRESSES}"
-else
+elif [[ "$UNOMI_DISTRIBUTION" == *elasticsearch* ]]; then
     # Elasticsearch configuration
+    SEARCH_ENGINE="elasticsearch"
     if [ "$UNOMI_ELASTICSEARCH_SSL_ENABLE" = 'true' ]; then
         schema='https'
     else
@@ -80,43 +78,49 @@ else
     health_endpoint="_cluster/health"
     # Build array of node URLs
     IFS=',' read -ra NODES <<< "${UNOMI_ELASTICSEARCH_ADDRESSES}"
+else
+    WAIT_SEARCH_ENGINE=false
+    echo "WARNING: unable to determine search engine from distribution name: $UNOMI_DISTRIBUTION"
+    echo "         Skipping waiting for engine to startup before starting Karaf, ensure it is expected"
 fi

-# Wait for search engine to be ready
-echo "Waiting for ${SEARCH_ENGINE} to be ready..."
-echo "Checking nodes: ${NODES[@]}"
-health_check=""
+# Wait for search engine to be ready (only if enabled)
+if [ "$WAIT_SEARCH_ENGINE" = true ]; then
+    echo "Waiting for ${SEARCH_ENGINE} to be ready..."
+    echo "Checking nodes: ${NODES[@]}"
+    health_check=""

-while ([ -z "$health_check" ] || ([ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ])); do
-    # Try each node until we get a successful response
-    for node in "${NODES[@]}"; do
-        node_url="${schema}://${node}/${health_endpoint}"
-        echo "Checking health at: ${node_url}"
-        health_check=$(check_node_health "$node_url" "$curl_opts")
+    while ([ -z "$health_check" ] || ([ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ])); do
+        # Try each node until we get a successful response
+        for node in "${NODES[@]}"; do
+            node_url="${schema}://${node}/${health_endpoint}"
+            echo "Checking health at: ${node_url}"
+            health_check=$(check_node_health "$node_url" "$curl_opts")

-        if [ ! -z "$health_check" ]; then
-            echo "Successfully connected to node: $node (status: ${health_check})"
-            break
+            if [ ! -z "$health_check" ]; then
+                echo "Successfully connected to node: $node (status: ${health_check})"
+                break
+            else
+                >&2 echo "Connection failed to node: $node"
+            fi
+        done
+
+        if [ -z "$health_check" ]; then
+            >&2 echo "${SEARCH_ENGINE^} is not yet available - all nodes unreachable"
+            sleep 3
+            continue
+        fi
+
+        if [ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ]; then
+            >&2 echo "${SEARCH_ENGINE^} health status: ${health_check} (waiting for yellow or green)"
+            sleep 3
         else
-            >&2 echo "Connection failed to node: $node"
+            >&2 echo "${SEARCH_ENGINE^} health status: ${health_check}"
         fi
     done

-    if [ -z "$health_check" ]; then
-        >&2 echo "${SEARCH_ENGINE^} is not yet available - all nodes unreachable"
-        sleep 3
-        continue
-    fi
-
-    if [ "$health_check" != 'yellow' ] && [ "$health_check" != 'green' ]; then
-        >&2 echo "${SEARCH_ENGINE^} health status: ${health_check} (waiting for yellow or green)"
-        sleep 3
-    else
-        >&2 echo "${SEARCH_ENGINE^} health status: ${health_check}"
-    fi
-done
-
-echo "${SEARCH_ENGINE^} is ready with health status: ${health_check}"
+    echo "${SEARCH_ENGINE^} is ready with health status: ${health_check}"
+fi

 # Run Unomi in current bash session
 exec "$UNOMI_HOME/bin/karaf" run
diff --git c/extensions/healthcheck/pom.xml i/extensions/healthcheck/pom.xml
index e16bd9ac7..e1fca175d 100644
--- c/extensions/healthcheck/pom.xml
+++ i/extensions/healthcheck/pom.xml
@@ -150,10 +150,17 @@
                             <artifacts>
                                 <artifact>
                                     <file>
-                                        src/main/resources/org.apache.unomi.healthcheck.cfg
+                                        src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg
                                     </file>
                                     <type>cfg</type>
-                                    <classifier>healthcheck</classifier>
+                                    <classifier>healthcheck-elasticsearch</classifier>
+                                </artifact>
+                                <artifact>
+                                    <file>
+                                        src/main/resources/org.apache.unomi.healthcheck-opensearch.cfg
+                                    </file>
+                                    <type>cfg</type>
+                                    <classifier>healthcheck-opensearch</classifier>
                                 </artifact>
                             </artifacts>
                         </configuration>
diff --git c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
index 278eb8a1e..b07f279e7 100644
--- c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
+++ i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
@@ -30,9 +30,14 @@ import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.util.EntityUtils;
 import org.apache.unomi.healthcheck.HealthCheckConfig;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
 import org.apache.unomi.healthcheck.HealthCheckResponse;
 import org.apache.unomi.healthcheck.util.CachedValue;
 import org.apache.unomi.shell.migration.utils.HttpUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;

@@ -43,13 +48,17 @@ import java.util.concurrent.TimeUnit;
  * A Health Check that checks the status of the ElasticSearch connectivity according to the provided configuration.
  * This connectivity should be LIVE before any try to start Unomi.
  */
-public class ElasticSearchHealthCheckProvider implements PersistenceEngineHealthProvider {
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class ElasticSearchHealthCheckProvider implements HealthCheckProvider {

     public static final String NAME = "elasticsearch";

     private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchHealthCheckProvider.class.getName());
     private final CachedValue<HealthCheckResponse> cache = new CachedValue<>(10, TimeUnit.SECONDS);
+    private final ObjectMapper mapper = new ObjectMapper();
+    private boolean isActive = false;

+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
     private HealthCheckConfig config;

     private CloseableHttpClient httpClient;
@@ -58,21 +67,25 @@ public class ElasticSearchHealthCheckProvider implements PersistenceEngineHealth
         LOGGER.info("Building elasticsearch health provider service...");
     }

+    @Activate
     public void activate() {
-        LOGGER.info("Activating elasticsearch health provider service...");
-        CredentialsProvider credentialsProvider = null;
-        String login = config.get(HealthCheckConfig.CONFIG_ES_LOGIN);
-        if (StringUtils.isNotEmpty(login)) {
-            credentialsProvider = new BasicCredentialsProvider();
-            UsernamePasswordCredentials credentials
-                    = new UsernamePasswordCredentials(login, config.get(HealthCheckConfig.CONFIG_ES_PASSWORD));
-            credentialsProvider.setCredentials(AuthScope.ANY, credentials);
-        }
-        try {
-            httpClient = HttpUtils.initHttpClient(
-                    Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_ES_TRUST_ALL_CERTIFICATES)), credentialsProvider);
-        } catch (IOException e) {
-            LOGGER.error("Unable to initialize http client", e);
+        if (config.isEnabled() && config.getEnabledProviders().stream().anyMatch(NAME::equals)) {
+            LOGGER.info("Activating elasticsearch health provider service...");
+            this.isActive = true;
+            CredentialsProvider credentialsProvider = null;
+            String login = config.get(HealthCheckConfig.CONFIG_ES_LOGIN);
+            if (StringUtils.isNotEmpty(login)) {
+                credentialsProvider = new BasicCredentialsProvider();
+                UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(login,
+                        config.get(HealthCheckConfig.CONFIG_ES_PASSWORD));
+                credentialsProvider.setCredentials(AuthScope.ANY, credentials);
+            }
+            try {
+                httpClient = HttpUtils.initHttpClient(Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_ES_TRUST_ALL_CERTIFICATES)),
+                        credentialsProvider);
+            } catch (IOException e) {
+                LOGGER.error("Unable to initialize http client", e);
+            }
         }
     }

@@ -92,56 +105,64 @@ public class ElasticSearchHealthCheckProvider implements PersistenceEngineHealth
         return cache.getValue();
     }

-    @Override public HealthCheckResponse detailed() {
-        return execute();
-    }
-
     private HealthCheckResponse refresh() {
         LOGGER.debug("Refresh");
         HealthCheckResponse.Builder builder = new HealthCheckResponse.Builder();
         builder.name(NAME).down();
-        String url = (config.get(HealthCheckConfig.CONFIG_ES_SSL_ENABLED).equals("true") ? "https://" : "http://")
-                        .concat(config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim())
-                        .concat("/_cluster/health");
-        CloseableHttpResponse response = null;
-        try {
-            response = httpClient.execute(new HttpGet(url));
-            if (response != null && response.getStatusLine().getStatusCode() == 200) {
-                builder.up();
-                HttpEntity entity = response.getEntity();
-                if (entity != null) {
-                    String content = EntityUtils.toString(entity);
-                    try {
-                        ObjectMapper mapper = new ObjectMapper();
-                        JsonNode root = mapper.readTree(content);
-                        if (root.has("status") && "green".equals(root.get("status").asText())) {
-                            builder.live();
-                        }
-                        if (root.has("cluster_name")) builder.withData("cluster_name", root.get("cluster_name").asText());
-                        if (root.has("status")) builder.withData("status", root.get("status").asText());
-                        if (root.has("timed_out")) builder.withData("timed_out", root.get("timed_out").asBoolean());
-                        if (root.has("number_of_nodes")) builder.withData("number_of_nodes", root.get("number_of_nodes").asLong());
-                        if (root.has("number_of_data_nodes")) builder.withData("number_of_data_nodes", root.get("number_of_data_nodes").asLong());
-                        if (root.has("active_primary_shards")) builder.withData("active_primary_shards", root.get("active_primary_shards").asLong());
-                        if (root.has("active_shards")) builder.withData("active_shards", root.get("active_shards").asLong());
-                        if (root.has("relocating_shards")) builder.withData("relocating_shards", root.get("relocating_shards").asLong());
-                        if (root.has("initializing_shards")) builder.withData("initializing_shards", root.get("initializing_shards").asLong());
-                        if (root.has("unassigned_shards")) builder.withData("unassigned_shards", root.get("unassigned_shards").asLong());
-                    } catch (Exception parseEx) {
-                        // Fallback to simple LIVE detection
-                        if (content.contains("\"status\":\"green\"")) {
-                            builder.live();
+        if (isActive) {
+            String url = (config.get(HealthCheckConfig.CONFIG_ES_SSL_ENABLED).equals("true") ? "https://" : "http://").concat(
+                    config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim()).concat("/_cluster/health");
+            CloseableHttpResponse response = null;
+            try {
+                response = httpClient.execute(new HttpGet(url));
+                if (response != null && response.getStatusLine().getStatusCode() == 200) {
+                    builder.up();
+                    HttpEntity entity = response.getEntity();
+                    if (entity != null) {
+                        String content = EntityUtils.toString(entity);
+                        try {
+                            JsonNode root = mapper.readTree(content);
+                            if (root.has("status") && "green".equals(root.get("status").asText())) {
+                                builder.live();
+                            }
+                            if (root.has("cluster_name"))
+                                builder.withData("cluster_name", root.get("cluster_name").asText());
+                            if (root.has("status"))
+                                builder.withData("status", root.get("status").asText());
+                            if (root.has("timed_out"))
+                                builder.withData("timed_out", root.get("timed_out").asBoolean());
+                            if (root.has("number_of_nodes"))
+                                builder.withData("number_of_nodes", root.get("number_of_nodes").asLong());
+                            if (root.has("number_of_data_nodes"))
+                                builder.withData("number_of_data_nodes", root.get("number_of_data_nodes").asLong());
+                            if (root.has("active_primary_shards"))
+                                builder.withData("active_primary_shards", root.get("active_primary_shards").asLong());
+                            if (root.has("active_shards"))
+                                builder.withData("active_shards", root.get("active_shards").asLong());
+                            if (root.has("relocating_shards"))
+                                builder.withData("relocating_shards", root.get("relocating_shards").asLong());
+                            if (root.has("initializing_shards"))
+                                builder.withData("initializing_shards", root.get("initializing_shards").asLong());
+                            if (root.has("unassigned_shards"))
+                                builder.withData("unassigned_shards", root.get("unassigned_shards").asLong());
+                        } catch (Exception parseEx) {
+                            // Fallback to simple LIVE detection
+                            if (content.contains("\"status\":\"green\"")) {
+                                builder.live();
+                            }
                         }
                     }
                 }
+            } catch (IOException e) {
+                builder.error().withData("error", e.getMessage());
+                LOGGER.error("Error while checking elasticsearch health", e);
+            } finally {
+                if (response != null) {
+                    EntityUtils.consumeQuietly(response.getEntity());
+                }
             }
-        } catch (IOException e) {
-            builder.error().withData("error", e.getMessage());
-            LOGGER.error("Error while checking elasticsearch health", e);
-        } finally {
-            if (response != null) {
-                EntityUtils.consumeQuietly(response.getEntity());
-            }
+        } else {
+            builder.error().withData("error", "Elasticsearch health check provider is not active");
         }
         return builder.build();
     }
diff --git c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java
index 7b97cfa12..f64f0df84 100644
--- c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java
+++ i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java
@@ -30,9 +30,14 @@ import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.util.EntityUtils;
 import org.apache.unomi.healthcheck.HealthCheckConfig;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
 import org.apache.unomi.healthcheck.HealthCheckResponse;
 import org.apache.unomi.healthcheck.util.CachedValue;
 import org.apache.unomi.shell.migration.utils.HttpUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;

@@ -43,13 +48,17 @@ import java.util.concurrent.TimeUnit;
  * A Health Check that checks the status of the OpenSearch connectivity according to the provided configuration.
  * This connectivity should be LIVE before any try to start Unomi.
  */
-public class OpenSearchHealthCheckProvider implements PersistenceEngineHealthProvider {
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class OpenSearchHealthCheckProvider implements HealthCheckProvider {

     public static final String NAME = "opensearch";

     private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchHealthCheckProvider.class.getName());
     private final CachedValue<HealthCheckResponse> cache = new CachedValue<>(10, TimeUnit.SECONDS);
+    private final ObjectMapper mapper = new ObjectMapper();
+    private boolean isActive = false;

+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
     private HealthCheckConfig config;

     private CloseableHttpClient httpClient;
@@ -58,21 +67,25 @@ public class OpenSearchHealthCheckProvider implements PersistenceEngineHealthPro
         LOGGER.info("Building OpenSearch health provider service...");
     }

+    @Activate
     public void activate() {
-        LOGGER.info("Activating OpenSearch health provider service...");
-        CredentialsProvider credentialsProvider = null;
-        String login = config.get(HealthCheckConfig.CONFIG_OS_LOGIN); // Reuse ElasticSearch credentials key
-        if (StringUtils.isNotEmpty(login)) {
-            credentialsProvider = new BasicCredentialsProvider();
-            UsernamePasswordCredentials credentials
-                    = new UsernamePasswordCredentials(login, config.get(HealthCheckConfig.CONFIG_OS_PASSWORD));
-            credentialsProvider.setCredentials(AuthScope.ANY, credentials);
-        }
-        try {
-            httpClient = HttpUtils.initHttpClient(
-                    Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_OS_TRUST_ALL_CERTIFICATES)), credentialsProvider);
-        } catch (IOException e) {
-            LOGGER.error("Unable to initialize http client", e);
+        if (config.isEnabled() && config.getEnabledProviders().stream().anyMatch(NAME::equals)) {
+            LOGGER.info("Activating OpenSearch health provider service...");
+            this.isActive = true;
+            CredentialsProvider credentialsProvider = null;
+            String login = config.get(HealthCheckConfig.CONFIG_OS_LOGIN); // Reuse ElasticSearch credentials key
+            if (StringUtils.isNotEmpty(login)) {
+                credentialsProvider = new BasicCredentialsProvider();
+                UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(login,
+                        config.get(HealthCheckConfig.CONFIG_OS_PASSWORD));
+                credentialsProvider.setCredentials(AuthScope.ANY, credentials);
+            }
+            try {
+                httpClient = HttpUtils.initHttpClient(Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_OS_TRUST_ALL_CERTIFICATES)),
+                        credentialsProvider);
+            } catch (IOException e) {
+                LOGGER.error("Unable to initialize http client", e);
+            }
         }
     }

@@ -92,63 +105,72 @@ public class OpenSearchHealthCheckProvider implements PersistenceEngineHealthPro
         return cache.getValue();
     }

-    @Override public HealthCheckResponse detailed() {
-        return execute();
-    }
-
     private HealthCheckResponse refresh() {
         LOGGER.debug("Refresh");
         HealthCheckResponse.Builder builder = new HealthCheckResponse.Builder();
         builder.name(NAME).down();
-        String minimalClusterState = config.get(HealthCheckConfig.CONFIG_OS_MINIMAL_CLUSTER_STATE);
-        if (StringUtils.isEmpty(minimalClusterState)) {
-            minimalClusterState = "green";
-        } else {
-            minimalClusterState = minimalClusterState.toLowerCase();
-        }
-        String url = (config.get(HealthCheckConfig.CONFIG_OS_SSL_ENABLED).equals("true") ? "https://" : "http://")
-                .concat(config.get(HealthCheckConfig.CONFIG_OS_ADDRESSES).split(",")[0].trim())
-                .concat("/_cluster/health");
-        CloseableHttpResponse response = null;
-        try {
-            response = httpClient.execute(new HttpGet(url));
-            if (response != null && response.getStatusLine().getStatusCode() == 200) {
-                builder.up();
-                HttpEntity entity = response.getEntity();
-                if (entity != null) {
-                    String content = EntityUtils.toString(entity);
-                    try {
-                        ObjectMapper mapper = new ObjectMapper();
-                        JsonNode root = mapper.readTree(content);
-                        String status = root.has("status") ? root.get("status").asText() : null;
-                        if ("green".equals(status) || ("yellow".equals(status) && "yellow".equals(minimalClusterState))) {
-                            builder.live();
-                        }
-                        if (root.has("cluster_name")) builder.withData("cluster_name", root.get("cluster_name").asText());
-                        if (root.has("status")) builder.withData("status", root.get("status").asText());
-                        if (root.has("timed_out")) builder.withData("timed_out", root.get("timed_out").asBoolean());
-                        if (root.has("number_of_nodes")) builder.withData("number_of_nodes", root.get("number_of_nodes").asLong());
-                        if (root.has("number_of_data_nodes")) builder.withData("number_of_data_nodes", root.get("number_of_data_nodes").asLong());
-                        if (root.has("active_primary_shards")) builder.withData("active_primary_shards", root.get("active_primary_shards").asLong());
-                        if (root.has("active_shards")) builder.withData("active_shards", root.get("active_shards").asLong());
-                        if (root.has("relocating_shards")) builder.withData("relocating_shards", root.get("relocating_shards").asLong());
-                        if (root.has("initializing_shards")) builder.withData("initializing_shards", root.get("initializing_shards").asLong());
-                        if (root.has("unassigned_shards")) builder.withData("unassigned_shards", root.get("unassigned_shards").asLong());
-                    } catch (Exception parseEx) {
-                        if (content.contains("\"status\":\"green\"") ||
-                                (content.contains("\"status\":\"yellow\"") && "yellow".equals(minimalClusterState))) {
-                            builder.live();
+        if (isActive) {
+            String minimalClusterState = config.get(HealthCheckConfig.CONFIG_OS_MINIMAL_CLUSTER_STATE);
+            if (StringUtils.isEmpty(minimalClusterState)) {
+                minimalClusterState = "green";
+            } else {
+                minimalClusterState = minimalClusterState.toLowerCase();
+            }
+            String url = (config.get(HealthCheckConfig.CONFIG_OS_SSL_ENABLED).equals("true") ? "https://" : "http://").concat(
+                    config.get(HealthCheckConfig.CONFIG_OS_ADDRESSES).split(",")[0].trim()).concat("/_cluster/health");
+            CloseableHttpResponse response = null;
+            try {
+                response = httpClient.execute(new HttpGet(url));
+                if (response != null && response.getStatusLine().getStatusCode() == 200) {
+                    builder.up();
+                    HttpEntity entity = response.getEntity();
+                    if (entity != null) {
+                        String content = EntityUtils.toString(entity);
+                        try {
+                            ObjectMapper mapper = new ObjectMapper();
+                            JsonNode root = mapper.readTree(content);
+                            String status = root.has("status") ? root.get("status").asText() : null;
+                            if ("green".equals(status) || ("yellow".equals(status) && "yellow".equals(minimalClusterState))) {
+                                builder.live();
+                            }
+                            if (root.has("cluster_name"))
+                                builder.withData("cluster_name", root.get("cluster_name").asText());
+                            if (root.has("status"))
+                                builder.withData("status", root.get("status").asText());
+                            if (root.has("timed_out"))
+                                builder.withData("timed_out", root.get("timed_out").asBoolean());
+                            if (root.has("number_of_nodes"))
+                                builder.withData("number_of_nodes", root.get("number_of_nodes").asLong());
+                            if (root.has("number_of_data_nodes"))
+                                builder.withData("number_of_data_nodes", root.get("number_of_data_nodes").asLong());
+                            if (root.has("active_primary_shards"))
+                                builder.withData("active_primary_shards", root.get("active_primary_shards").asLong());
+                            if (root.has("active_shards"))
+                                builder.withData("active_shards", root.get("active_shards").asLong());
+                            if (root.has("relocating_shards"))
+                                builder.withData("relocating_shards", root.get("relocating_shards").asLong());
+                            if (root.has("initializing_shards"))
+                                builder.withData("initializing_shards", root.get("initializing_shards").asLong());
+                            if (root.has("unassigned_shards"))
+                                builder.withData("unassigned_shards", root.get("unassigned_shards").asLong());
+                        } catch (Exception parseEx) {
+                            if (content.contains("\"status\":\"green\"") || (content.contains("\"status\":\"yellow\"") && "yellow".equals(
+                                    minimalClusterState))) {
+                                builder.live();
+                            }
                         }
                     }
                 }
+            } catch (IOException e) {
+                builder.error().withData("error", e.getMessage());
+                LOGGER.error("Error while checking OpenSearch health", e);
+            } finally {
+                if (response != null) {
+                    EntityUtils.consumeQuietly(response.getEntity());
+                }
             }
-        } catch (IOException e) {
-            builder.error().withData("error", e.getMessage());
-            LOGGER.error("Error while checking OpenSearch health", e);
-        } finally {
-            if (response != null) {
-                EntityUtils.consumeQuietly(response.getEntity());
-            }
+        } else {
+            builder.error().withData("error", "Elasticsearch health check provider is not active");
         }
         return builder.build();
     }
diff --git c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceEngineHealthProvider.java i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceEngineHealthProvider.java
deleted file mode 100644
index 276ba2272..000000000
--- c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceEngineHealthProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.unomi.healthcheck.provider;
-
-import org.apache.unomi.healthcheck.HealthCheckProvider;
-import org.apache.unomi.healthcheck.HealthCheckResponse;
-
-/**
- * Common contract for persistence engine health providers to expose
- * richer, implementation-specific health details.
- */
-public interface PersistenceEngineHealthProvider extends HealthCheckProvider {
-
-    /**
-     * Build a detailed response that may include implementation-specific data.
-     *
-     * @return a detailed {@link HealthCheckResponse}
-     */
-    HealthCheckResponse detailed();
-}
-
-
diff --git c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
index ec136fa33..b0d2725dc 100644
--- c/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
+++ i/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
@@ -20,7 +20,6 @@ package org.apache.unomi.healthcheck.provider;
 import org.apache.unomi.api.PropertyType;
 import org.apache.unomi.healthcheck.HealthCheckResponse;
 import org.apache.unomi.healthcheck.HealthCheckProvider;
-import org.apache.unomi.healthcheck.HealthCheckConfig;
 import org.apache.unomi.healthcheck.util.CachedValue;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.osgi.service.component.annotations.Component;
@@ -47,25 +46,16 @@ public class PersistenceHealthCheckProvider implements HealthCheckProvider {
     @Reference(service = PersistenceService.class, cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, bind = "bind", unbind = "unbind")
     private volatile PersistenceService service;

-    @Reference(cardinality = ReferenceCardinality.OPTIONAL)
-    private volatile HealthCheckConfig healthCheckConfig;
-
-    // Lazily created delegate depending on the current persistence implementation
-    private volatile HealthCheckProvider delegate;
-
     public PersistenceHealthCheckProvider() {
         LOGGER.info("Building persistence health provider service...");
     }

     public void bind(PersistenceService service) {
         this.service = service;
-        // Reset delegate when persistence changes
-        this.delegate = null;
     }

     public void unbind(PersistenceService service) {
         this.service = null;
-        this.delegate = null;
     }

     @Override public String name() {
@@ -74,17 +64,6 @@ public class PersistenceHealthCheckProvider implements HealthCheckProvider {

     @Override public HealthCheckResponse execute() {
         LOGGER.debug("Health check persistence");
-
-        // If we can detect the underlying persistence, delegate to the appropriate provider
-        HealthCheckProvider resolved = resolveDelegate();
-        if (resolved != null) {
-            if (resolved instanceof PersistenceEngineHealthProvider) {
-                return ((PersistenceEngineHealthProvider) resolved).detailed();
-            }
-            return resolved.execute();
-        }
-
-        // Fallback to legacy behavior if no delegate is available yet
         if (cache.isStaled() || cache.getValue().isDown() || cache.getValue().isError()) {
             cache.setValue(refresh());
         }
@@ -109,49 +88,4 @@ public class PersistenceHealthCheckProvider implements HealthCheckProvider {
         }
         return builder.build();
     }
-
-    private HealthCheckProvider resolveDelegate() {
-        try {
-            if (delegate != null) {
-                return delegate;
-            }
-            if (service == null) {
-                return null;
-            }
-            String persistenceName;
-            try {
-                persistenceName = service.getName();
-            } catch (Throwable t) {
-                // Older SPI might not expose getName(); fallback to class inspection
-                persistenceName = service.getClass().getName().toLowerCase();
-            }
-
-            if (persistenceName == null) {
-                return null;
-            }
-
-            if (persistenceName.contains("opensearch")) {
-                OpenSearchHealthCheckProvider provider = new OpenSearchHealthCheckProvider();
-                if (healthCheckConfig != null) {
-                    provider.setConfig(healthCheckConfig);
-                }
-                provider.activate();
-                delegate = provider;
-            } else if (persistenceName.contains("elasticsearch")) {
-                ElasticSearchHealthCheckProvider provider = new ElasticSearchHealthCheckProvider();
-                if (healthCheckConfig != null) {
-                    provider.setConfig(healthCheckConfig);
-                }
-                provider.activate();
-                delegate = provider;
-            } else {
-                // Unknown persistence implementation, no delegate
-                return null;
-            }
-            return delegate;
-        } catch (Exception e) {
-            LOGGER.warn("Unable to resolve delegated health check provider", e);
-            return null;
-        }
-    }
 }
diff --git c/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg i/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg
new file mode 100644
index 000000000..20a8c2a6b
--- /dev/null
+++ i/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Elasticsearch configuration
+esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200}
+esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false}
+esLogin = ${org.apache.unomi.elasticsearch.username:-}
+esPassword = ${org.apache.unomi.elasticsearch.password:-}
+esHttpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false}
+
+# Security configuration
+authentication.realm = ${org.apache.unomi.security.realm:-karaf}
+
+# Health check configuration
+healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-false}
+healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,unomi,persistence}
+healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400}
diff --git c/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg i/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-opensearch.cfg
similarity index 78%
rename from extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg
rename to extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-opensearch.cfg
index 9c6083ab9..98fef8fdf 100644
--- c/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg
+++ i/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-opensearch.cfg
@@ -15,13 +15,6 @@
 # limitations under the License.
 #

-# Elasticsearch configuration
-esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200}
-esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false}
-esLogin = ${org.apache.unomi.elasticsearch.username:-}
-esPassword = ${org.apache.unomi.elasticsearch.password:-}
-esHttpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false}
-
 # OpenSearch configuration
 osAddresses = ${org.apache.unomi.opensearch.addresses:-localhost:9200}
 osSSLEnabled = ${org.apache.unomi.opensearch.sslEnable:-true}
@@ -35,5 +28,5 @@ authentication.realm = ${org.apache.unomi.security.realm:-karaf}

 # Health check configuration
 healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-false}
-healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,opensearch,unomi,persistence}
+healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,opensearch,unomi,persistence}
 healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400}
diff --git c/itests/src/test/java/org/apache/unomi/itests/BaseIT.java i/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index d7b6de159..9bb734c91 100644
--- c/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ i/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -182,7 +182,8 @@ public abstract class BaseIT extends KarafTestSupport {
             } else if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)){
                 LOGGER.info("Starting Unomi with opensearch search engine...");
                 System.out.println("==== Starting Unomi with opensearch search engine...");
-                executeCommand("unomi:start " + SEARCH_ENGINE_OPENSEARCH);
+                executeCommand("unomi:setup -d unomi-distribution-opensearch --force true");
+                executeCommand("unomi:start");
             } else {
                 LOGGER.error("Unknown search engine: " + searchEngine);
                 throw new InterruptedException("Unknown search engine: " + searchEngine);
@@ -306,7 +307,7 @@ public abstract class BaseIT extends KarafTestSupport {
                     "unomi-shell-dev-commands",
                     "unomi-wab",
                     "unomi-web-tracker",
-                    "unomi-healthcheck",
+                    "unomi-healthcheck-elasticsearch",
                     "unomi-router-karaf-feature",
                     "unomi-groovy-actions",
                     "unomi-rest-ui",
@@ -332,7 +333,7 @@ public abstract class BaseIT extends KarafTestSupport {
                     "unomi-shell-dev-commands",
                     "unomi-wab",
                     "unomi-web-tracker",
-                    "unomi-healthcheck",
+                    "unomi-healthcheck-opensearch",
                     "unomi-router-karaf-feature",
                     "unomi-groovy-actions",
                     "unomi-rest-ui",
diff --git c/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java i/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
index 882259525..d68bcd6f1 100644
--- c/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
+++ i/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
@@ -59,10 +59,11 @@ public class HealthCheckIT extends BaseIT {
             List<HealthCheckResponse> response = get(HEALTHCHECK_ENDPOINT, new TypeReference<>() {});
             LOGGER.info("health check response: {}", response);
             Assert.assertNotNull(response);
-            Assert.assertEquals(4, response.size());
+            Assert.assertEquals(5, response.size());
             Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("karaf") && r.getStatus() == HealthCheckResponse.Status.LIVE));
             Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals(searchEngine) && r.getStatus() == HealthCheckResponse.Status.LIVE));
             Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("unomi") && r.getStatus() == HealthCheckResponse.Status.LIVE));
+            Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("persistence") && r.getStatus() == HealthCheckResponse.Status.LIVE));
             Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("cluster") && r.getStatus() == HealthCheckResponse.Status.LIVE));
         } catch (Exception e) {
             LOGGER.error("Error while executing health check", e);
diff --git c/itests/src/test/resources/org.apache.unomi.healthcheck.cfg i/itests/src/test/resources/org.apache.unomi.healthcheck.cfg
index 9de18615b..4f5e824ea 100644
--- c/itests/src/test/resources/org.apache.unomi.healthcheck.cfg
+++ i/itests/src/test/resources/org.apache.unomi.healthcheck.cfg
@@ -35,5 +35,5 @@ authentication.realm = ${org.apache.unomi.security.realm:-karaf}

 # Health check configuration
 healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-true}
-healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,opensearch,unomi,persistence}
+healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,unomi,persistence}
 healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400}
diff --git c/kar/src/main/feature/feature.xml i/kar/src/main/feature/feature.xml
index d4f83fe94..ed3d3a483 100644
--- c/kar/src/main/feature/feature.xml
+++ i/kar/src/main/feature/feature.xml
@@ -75,7 +75,6 @@
     <feature name="unomi-startup" description="Apache Unomi :: Startup Tools" version="${project.version}">
         <feature>unomi-base</feature>
         <configfile finalname="/etc/org.apache.unomi.migration.cfg">mvn:org.apache.unomi/shell-commands/${project.version}/cfg/migration</configfile>
-        <configfile finalname="/etc/org.apache.unomi.start.cfg">mvn:org.apache.unomi/shell-commands/${project.version}/cfg/start</configfile>
         <bundle>mvn:org.apache.unomi/shell-commands/${project.version}</bundle>
     </feature>

@@ -184,9 +183,15 @@
         <bundle start="false">mvn:org.apache.unomi/unomi-web-tracker-wab/${project.version}</bundle>
     </feature>

-    <feature name="unomi-healthcheck" description="Apache Unomi :: Healthcheck" version="${project.version}">
+    <feature name="unomi-healthcheck-elasticsearch" description="Apache Unomi :: Healthcheck ElasticSearch" version="${project.version}">
         <feature>unomi-web-tracker</feature>
-        <configfile finalname="/etc/org.apache.unomi.healthcheck.cfg">mvn:org.apache.unomi/healthcheck/${project.version}/cfg/healthcheck</configfile>
+        <configfile finalname="/etc/org.apache.unomi.healthcheck.cfg">mvn:org.apache.unomi/healthcheck/${project.version}/cfg/healthcheck-elasticsearch</configfile>
+        <bundle start="false">mvn:org.apache.unomi/healthcheck/${project.version}</bundle>
+    </feature>
+
+    <feature name="unomi-healthcheck-opensearch" description="Apache Unomi :: Healthcheck OpenSearch" version="${project.version}">
+        <feature>unomi-web-tra…
diff --git c/manual/src/main/asciidoc/configuration.adoc i/manual/src/main/asciidoc/configuration.adoc
index b3d97a9..4d923ad 100644
--- c/manual/src/main/asciidoc/configuration.adoc
+++ i/manual/src/main/asciidoc/configuration.adoc
@@ -876,60 +876,143 @@ The following permissions are required by Unomi:
  - required cluster privileges: `manage` OR `all`
  - required index privileges on unomi indices: `write, manage, read` OR `all`

-=== Customizing Start Features Configuration
+=== Unomi Distribution (features configuration)

-Apache Unomi allows you to customize which features and bundles are installed and started when using the `unomi:start` command. This is controlled through the `org.apache.unomi.start.cfg` configuration file.
+You can define a specific distribution for Apache Unomi to configure desired features when the server boots up. Is it a classic Karaf feature that defines desired features for Unomi.
+Be aware that, even if a distribution is a classic Karaf feature XML file, it must only be composed feature's dependencies as this file is interpreted by the Unomi ManagementService.
+No bundle nor config file will be processed from this file.

-==== Default Configuration
+To set the desired distribution, set the `unomi.distribution` system property or `UNOMI_DISTRIBUTION` environment variable to the name of your desired distribution.
+You can also use the dedicated `unomi:setup` command to set the distribution interactively.

-By default, Apache Unomi comes with two predefined start features configurations:
+Apache Unomi comes with some predefined distributions that you can use directly.

-[source]
-----
-startFeatures = [
-    "elasticsearch=unomi-base,unomi-startup,unomi-elasticsearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-elasticsearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete",
-    "opensearch=unomi-base,unomi-startup,unomi-opensearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-opensearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete"
-]
-----
+==== Default Distributions

-**Key Differences Between Configurations:**
+By default, Apache Unomi comes with four predefined distributions:
+
+unomi-distribution-elasticsearch, unomi-distribution-elasticsearch-graphql, unomi-distribution-opensearch, unomi-distribution-opensearch-graphql
+
+**Key Differences Between Distributions:**

 The only difference between the Elasticsearch and OpenSearch configurations is the persistence layer:

 * **Elasticsearch**: Uses `unomi-elasticsearch-core` and `unomi-elasticsearch-conditions`
 * **OpenSearch**: Uses `unomi-opensearch-core` and `unomi-opensearch-conditions`

-All other features remain identical between both configurations.
+All other features remain identical between both configurations. Each one is derived with or without GraphQL support.

-==== Environment-Specific Configurations
+==== Environment-Specific Distributions

-You can create different configurations for different deployment environments by including or excluding certain features:
+You can create different distributions for different deployment environments by creating a dedicated distribution feature.
+
+Be aware that in distribution's feature, you should only reference other features, not bundles or configuration directly.

 **Development Environment** (includes development tools and debugging features):
-[source]
+[source,xml]
 ----
-startFeatures = [
-    "elasticsearch-dev=unomi-base,unomi-startup,unomi-elasticsearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-elasticsearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete",
-    "opensearch-dev=unomi-base,unomi-startup,unomi-opensearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-opensearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete"
-]
+<features name="unomi-distributions" xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.6.0 https://karaf.apache.org/xmlns/features/v1.6.0">
+
+    <repository>mvn:org.apache.karaf.features/specs/${karaf.version}/xml/features</repository>
+    <repository>mvn:org.apache.cxf.karaf/apache-cxf/${cxf.version}/xml/features</repository>
+    <repository>mvn:org.apache.unomi/unomi-kar/${project.version}/xml/features</repository>
+
+    <feature name="unomi-distribution-elasticsearch-dev" description="Apache Unomi :: ElasticSearch Development Distribution" version="${project.version}">
+        <feature dependency="true" version="${project.version}">unomi-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-persistence-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-services</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-api</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-lists-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-geonames-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-privacy-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-conditions</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-request</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-mail</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-optimization-test</feature>
+        <feature dependency="true" version="${project.version}">unomi-shell-dev-commands</feature>
+        <feature dependency="true" version="${project.version}">unomi-wab</feature>
+        <feature dependency="true" version="${project.version}">unomi-web-tracker</feature>
+        <feature dependency="true" version="${project.version}">unomi-healthcheck-elasticsearch</feature>
+        <feature dependency="true" version="${project.version}">unomi-router-karaf-feature</feature>
+        <feature dependency="true" version="${project.version}">unomi-groovy-actions</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-ui</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup-complete</feature>
+    </feature>
+</features>
 ----

 **Staging Environment** (production-like but with some development features):
-[source]
+[source,xml]
 ----
-startFeatures = [
-    "elasticsearch-staging=unomi-base,unomi-startup,unomi-elasticsearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-elasticsearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete",
-    "opensearch-staging=unomi-base,unomi-startup,unomi-opensearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-opensearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete"
-]
+<features name="unomi-distributions" xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.6.0 https://karaf.apache.org/xmlns/features/v1.6.0">
+
+    <repository>mvn:org.apache.karaf.features/specs/${karaf.version}/xml/features</repository>
+    <repository>mvn:org.apache.cxf.karaf/apache-cxf/${cxf.version}/xml/features</repository>
+    <repository>mvn:org.apache.unomi/unomi-kar/${project.version}/xml/features</repository>
+
+    <feature name="unomi-distribution-elasticsearch-staging" description="Apache Unomi :: ElasticSearch Staging Distribution" version="${project.version}">
+        <feature dependency="true" version="${project.version}">unomi-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-persistence-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-services</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-api</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-lists-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-geonames-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-privacy-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-conditions</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-request</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-mail</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-optimization-test</feature>
+        <feature dependency="true" version="${project.version}">unomi-shell-dev-commands</feature>
+        <feature dependency="true" version="${project.version}">unomi-wab</feature>
+        <feature dependency="true" version="${project.version}">unomi-web-tracker</feature>
+        <feature dependency="true" version="${project.version}">unomi-healthcheck-elasticsearch</feature>
+        <feature dependency="true" version="${project.version}">unomi-router-karaf-feature</feature>
+        <feature dependency="true" version="${project.version}">unomi-groovy-actions</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-ui</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup-complete</feature>
+    </feature>
+</features>
 ----

 **Production Environment** (minimal, secure configuration):
-[source]
+[source,xml]
 ----
-startFeatures = [
-    "elasticsearch-prod=unomi-base,unomi-startup,unomi-elasticsearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-elasticsearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete",
-    "opensearch-prod=unomi-base,unomi-startup,unomi-opensearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-opensearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete"
-]
+<features name="unomi-distributions" xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.6.0 https://karaf.apache.org/xmlns/features/v1.6.0">
+
+    <repository>mvn:org.apache.karaf.features/specs/${karaf.version}/xml/features</repository>
+    <repository>mvn:org.apache.cxf.karaf/apache-cxf/${cxf.version}/xml/features</repository>
+    <repository>mvn:org.apache.unomi/unomi-kar/${project.version}/xml/features</repository>
+
+    <feature name="unomi-distribution-elasticsearch-prod" description="Apache Unomi :: ElasticSearch Production Distribution" version="${project.version}">
+        <feature dependency="true" version="${project.version}">unomi-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-persistence-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-services</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-api</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-lists-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-geonames-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-privacy-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-conditions</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-request</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-mail</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-optimization-test</feature>
+        <feature dependency="true" version="${project.version}">unomi-wab</feature>
+        <feature dependency="true" version="${project.version}">unomi-web-tracker</feature>
+        <feature dependency="true" version="${project.version}">unomi-healthcheck-elasticsearch</feature>
+        <feature dependency="true" version="${project.version}">unomi-router-karaf-feature</feature>
+        <feature dependency="true" version="${project.version}">unomi-groovy-actions</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-ui</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup-complete</feature>
+    </feature>
+</features>
 ----

 **Environment Differences Summary:**
@@ -949,37 +1032,32 @@ startFeatures = [
 |✗ (excluded)
 |===

-==== Configuration Format
+==== Distribution Feature Format

-Each start features configuration follows this format:
+Each start features configuration follows the classic Karaf features XML format, but you must only reference other features in your distribution feature.
+If you reference bundles or configuration files directly, they will be ignored by Unomi.
+
+To use a custom distribution, the corresponding feature must be accessible by Karaf. It can be done by publishing the feature maven artefact and adding it as a feature repository in Karaf os for any other feature. Of course you can also add your distribution feature in the existing source code and repackage Unomi making it available directly.
+
+==== Using Custom Distributions
+
+You can use your custom distribution with:

 [source]
 ----
-"configuration-name=feature1,feature2,feature3,..."
+# Using system property
+-Dunomi.distribution=unomi-distribution-elasticsearch
+
+# Using environment variable
+UNOMI_DISTRIBUTION=unomi-distribution-opensearch
+
+# Using the setup command
+unomi:setup unomi-distribution-elasticsearch-staging
 ----

-Where:
-* `configuration-name` is the identifier you'll use with the `unomi:start` command
-* The comma-separated list contains the Karaf features to install and start
-
-==== Using Custom Configurations
-
-You can use your custom configurations with:
-
-[source]
-----
-# Development environment
-unomi:start elasticsearch-dev
-unomi:start opensearch-dev
-
-# Staging environment
-unomi:start elasticsearch-staging
-unomi:start opensearch-staging
-
-# Production environment
-unomi:start elasticsearch-prod
-unomi:start opensearch-prod
-----
+Once you have called the `unomi:setup` command with your desired distribution, you can start Apache Unomi using the `unomi:start` command.
+The setup command only needs to be called once unless you want to change the distribution ; it creates a marker file to remember the chosen distribution.
+In case you want to change it, you can use the `unomi:setup --force` command again with the desired distribution name (using the --force option will override the existing distribution).

 ==== Auto-Start

@@ -989,27 +1067,17 @@ To enable auto-start, set the `unomi.autoStart` system property or `UNOMI_AUTO_S

 Note: Auto-start only works when Apache Unomi is not already running. If the server is already started, the auto-start setting will be ignored.

-==== Unomi Distribution
+=== Customizing Apache Unomi Distribution

-You can define a specific distribution for Apache Unomi to configure desired features when the server boots up. Is it a classic Karaf feature that defines desired features for Unomi.
-Be aware that, even if a distribution is a classic Karaf feature XML file, it must only be composed feature's dependencies as this file is interpreted by the Unomi ManagementService.
-No bundle nor config file will be processed from this file.
+Apache Unomi allows you to create custom distributions by repackaging the standard distribution with your own configuration files and customizations. This is useful for creating deployment-specific packages that include your custom configurations, plugins, and settings.

-To set the desired distribution, set the `unomi.distribution` system property or `UNOMI_DISTRIBUTION` environment variable to the name of your desired distribution:
+==== Note on Custom Distributions (Advanced)

-[source]
-----
-# Using system property
--Dunomi.distribution=unomi-distribution-elasticsearch
+You can build custom distributions of Unomi using the Karaf Maven Plugin to assemble features and overlay configuration. This is an advanced path and not required for most users. We recommend Docker-based packaging and configuration overrides as documented above.

-# Using environment variable (Docker)
-UNOMI_DISTRIBUTION=unomi-distribution-opensearch
+For details about custom distributions, see the Karaf documentation (Karaf Maven Plugin):

-# Using custom distribution
-UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch-staging
-----
-
-To use a custom distribution, the corresponding feature must be accessible by Karaf. It can be done by publishing the feature maven artefact and adding it as a feature repository in Karaf os for any other feature. Of course you can also add your distribution feature in the existing source code and repackage Unomi making it available directly.
+`https://karaf.apache.org/manual/latest/#_karaf_maven_plugin`

 ==== Important Notes

@@ -1059,18 +1127,6 @@ services:
 - Declare the feature repository in Karaf using KARAF_FEATURES_REPOSITORIES environment variable pointing to your mounted file or a online maven artifact (mvn:com.example/custom-unomi-distribs/1.0.0/xml/features)
 - Use the `UNOMI_DISTRIBUTION` environment variable to specify which distribution (using the feature name) to use from that feature's repository

-=== Customizing Apache Unomi Distribution
-
-Apache Unomi allows you to create custom distributions by repackaging the standard distribution with your own configuration files and customizations. This is useful for creating deployment-specific packages that include your custom configurations, plugins, and settings.
-
-==== Note on Custom Distributions (Advanced)
-
-You can build custom distributions of Unomi using the Karaf Maven Plugin to assemble features and overlay configuration. This is an advanced path and not required for most users. We recommend Docker-based packaging and configuration overrides as documented above.
-
-For details about custom distributions, see the Karaf documentation (Karaf Maven Plugin):
-
-`https://karaf.apache.org/manual/latest/#_karaf_maven_plugin`
-
 ==== Configuration Override Priority

 Apache Unomi follows this priority order for configuration files (highest to lowest priority):
diff --git c/manual/src/main/asciidoc/shell-commands.adoc i/manual/src/main/asciidoc/shell-commands.adoc
index 5bcb32d..515d63c 100644
--- c/manual/src/main/asciidoc/shell-commands.adoc
+++ i/manual/src/main/asciidoc/shell-commands.adoc
@@ -65,6 +65,11 @@ The commands control the lifecycle of the Apache Unomi server and are used to mi
 |===
 |Command|Arguments|Description

+|setup
+|distribution-feature-name,--force
+|This command must be used only when the Apache Unomi application is NOT STARTED. It will perform initial setup of the application
+using the specified distribution feature name (unomi-distribution-elasticsearch for example).
+
 |migrate
 |fromVersion
 |This command must be used only when the Apache Unomi application is NOT STARTED. It will perform migration of the data stored in search engine using the argument fromVersion as a starting point.
@@ -74,8 +79,8 @@ The commands control the lifecycle of the Apache Unomi server and are used to mi
 |Shutsdown the Apache Unomi application

 |start
-|startFeatures
-|Starts the Apache Unomi application with the specified start features configuration (elasticsearch or opensearch). Note that this state will be remembered between Apache Karaf launches, so in general it is only needed after a first installation or after a `migrate` command
+|n/a
+|Starts the Apache Unomi application with the specified distribution (or by default unomi-distribution-elasticsearch). Note that this state will be remembered between Apache Karaf launches, so in general it is only needed after a first installation

 |version
 |n/a
@@ -195,4 +200,4 @@ and interactive mode except that it undeploys definitions instead of deploying t
 working on a plugin. For example to remove all the definitions deployed by a plugin you can simply use the following
 command: `undeploy-definition BUNDLE_ID * *` when `BUNDLE_ID` is the identifier of the bundle that contains your plugin.

-|===
\ No newline at end of file
+|===
diff --git c/manual/src/main/asciidoc/writing-plugins.adoc i/manual/src/main/asciidoc/writing-plugins.adoc
index c2b49fe..b485f0c 100644
--- c/manual/src/main/asciidoc/writing-plugins.adoc
+++ i/manual/src/main/asciidoc/writing-plugins.adoc
@@ -162,14 +162,40 @@ OpenSearch Bundle (my-plugin-opensearch):

 ==== Configuration and Deployment

-Administrators can control which search engine implementation to use through the `org.apache.unomi.start.cfg` configuration file. This file determines which features (including your plugin's bundles) are deployed based on the chosen search engine:
+Administrators can control which search engine implementation to use by setting up a distribution. This distribution 'macro' feature determines which features (including your plugin's bundles) are deployed based on the chosen search engine:

-[source]
+[source,xml]
 ----
-startFeatures = [
-    "elasticsearch=unomi-persistence-elasticsearch,my-plugin-elasticsearch,unomi-services,...",
-    "opensearch=unomi-persistence-opensearch,my-plugin-opensearch,unomi-services,..."
-]
+<features>
+    <feature name="my-plugin-feature" version="${project.version}">
+        <bundle>mvn:my.group.id/my-plugin-common/${project.version}</bundle>
+        <bundle>mvn:my.group.id/my-plugin-${search.engine}/${project.version}</bundle>
+    </feature>
+
+    <feature name="my-unomi-distribution-with-my-plugin" version="${project.version}">
+        <feature dependency="true" version="${project.version}">unomi-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-persistence-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-services</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-api</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-lists-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-geonames-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-privacy-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-conditions</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-request</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-mail</feature>
+        <feature dependency="true" version="${project.version}">unomi-wab</feature>
+        <feature dependency="true" version="${project.version}">unomi-web-tracker</feature>
+        <feature dependency="true" version="${project.version}">unomi-healthcheck-elasticsearch</feature>
+        <feature dependency="true" version="${project.version}">unomi-router-karaf-feature</feature>
+        <feature dependency="true" version="${project.version}">unomi-groovy-actions</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-ui</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup-complete</feature>
+        <feature dependency="true" version="${project.version}">my-plugin-feature</feature>
+    </feature>
+</features>
 ----

 ==== Custom Plugins
@@ -680,14 +706,14 @@ import org.apache.unomi.persistence.elasticsearch.ConditionESQueryBuilderDispatc
 import java.util.Map;

 public class MyCustomQueryBuilder implements ConditionESQueryBuilder {
-
+
     @OverRide
-    public Query buildQuery(Condition condition, Map<String, Object> context,
+    public Query buildQuery(Condition condition, Map<String, Object> context,
                            ConditionESQueryBuilderDispatcher dispatcher) {
         // Get parameters from the condition
         String fieldName = (String) condition.getParameter("fieldName");
         String fieldValue = (String) condition.getParameter("fieldValue");
-
+
         // Build Elasticsearch-specific query using the new client
         return Query.of(q -> q
             .bool(b -> b
@@ -700,9 +726,9 @@ public class MyCustomQueryBuilder implements ConditionESQueryBuilder {
             )
         );
     }
-
+
     @OverRide
-    public long count(Condition condition, Map<String, Object> context,
+    public long count(Condition condition, Map<String, Object> context,
                      ConditionESQueryBuilderDispatcher dispatcher) {
         // Implement count logic if needed
         return 0;
@@ -723,14 +749,14 @@ import org.apache.unomi.persistence.opensearch.ConditionOSQueryBuilderDispatcher
 import java.util.Map;

 public class MyCustomQueryBuilder implements ConditionOSQueryBuilder {
-
+
     @OverRide
-    public Query buildQuery(Condition condition, Map<String, Object> context,
+    public Query buildQuery(Condition condition, Map<String, Object> context,
                            ConditionOSQueryBuilderDispatcher dispatcher) {
         // Get parameters from the condition
         String fieldName = (String) condition.getParameter("fieldName");
         String fieldValue = (String) condition.getParameter("fieldValue");
-
+
         // Build OpenSearch-specific query
         return Query.of(q -> q
             .bool(b -> b
@@ -743,9 +769,9 @@ public class MyCustomQueryBuilder implements ConditionOSQueryBuilder {
             )
         );
     }
-
+
     @OverRide
-    public long count(Condition condition, Map<String, Object> context,
+    public long count(Condition condition, Map<String, Object> context,
                      ConditionOSQueryBuilderDispatcher dispatcher) {
         // Implement count logic if needed
         return 0;
@@ -800,7 +826,7 @@ public class MyCustomQueryBuilder implements ConditionOSQueryBuilder {
       "multivalued": false
     },
     {
-      "id": "fieldValue",
+      "id": "fieldValue",
       "type": "string",
       "multivalued": false
     }
@@ -854,14 +880,14 @@ my-custom-plugin/
     <artifactId>my-custom-plugin</artifactId>
     <version>1.0.0</version>
     <packaging>pom</packaging>
-
+
     <modules>
         <module>my-custom-plugin-common</module>
         <module>my-custom-plugin-elasticsearch</module>
         <module>my-custom-plugin-opensearch</module>
         <module>my-custom-plugin-features</module>
     </modules>
-
+
     <properties>
         <unomi.version>3.0.0</unomi.version>
     </properties>
@@ -877,10 +903,10 @@ my-custom-plugin/
         <artifactId>my-custom-plugin</artifactId>
         <version>1.0.0</version>
     </parent>
-
+
     <artifactId>my-custom-plugin-elasticsearch</artifactId>
     <packaging>bundle</packaging>
-
+
     <dependencies>
         <dependency>
             <groupId>org.apache.unomi</groupId>
@@ -898,7 +924,7 @@ my-custom-plugin/
             <version>1.0.0</version>
         </dependency>
     </dependencies>
-
+
     <build>
         <plugins>
             <plugin>
@@ -926,14 +952,14 @@ my-custom-plugin/
 ----
 <?xml version="1.0" encoding="UTF-8"?>
 <features name="my-custom-plugin" version="1.0.0">
-
+
     <!-- Elasticsearch feature -->
     <feature name="my-custom-plugin-elasticsearch" version="1.0.0">
         <bundle>mvn:org.apache.unomi.plugins/my-custom-plugin-common/1.0.0</bundle>
         <bundle>mvn:org.apache.unomi.plugins/my-custom-plugin-elasticsearch/1.0.0</bundle>
         <feature>unomi-elasticsearch</feature>
     </feature>
-
+
     <!-- OpenSearch feature -->
     <feature name="my-custom-plugin-opensearch" version="1.0.0">
         <bundle>mvn:org.apache.unomi.plugins/my-custom-plugin-common/1.0.0</bundle>
@@ -981,24 +1007,43 @@ my-custom-plugin/
      - For OpenSearch: `feature:install my-custom-plugin-opensearch`
    - Document dependencies and requirements clearly

-==== Custom Start Configuration
+==== Custom Distribution Feature

-For production deployments, you can create a custom `org.apache.unomi.start.cfg` file to automatically include your plugin features in the startup configuration. This approach ensures your plugin is automatically deployed when Apache Unomi starts.
+For production deployments, you can create a custom distribution's feature file to automatically include your plugin features in the startup configuration. This approach ensures your plugin is automatically deployed when Apache Unomi starts.

-**Creating a Custom Start Configuration:**
+**Creating a Custom Distribution Feature:**

-1. **Create the configuration file** in your deployment directory:
-   ```
-   etc/org.apache.unomi.start.cfg
-   ```
+1. **Create the feature file** in a dedicated maven module. You can use the unomi-distribution module as a reference.

-2. **Define your custom configurations** by extending the default ones:
+2. **Define your custom distribution** by extending the default ones:

-[source,properties]
+[source,xml]
 ----
-# Custom start configurations that include your plugin features
-startFeatures = [ "elasticsearch=unomi-base,unomi-startup,unomi-elasticsearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-elasticsearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete,my-custom-plugin-elasticsearch", \
-                  "opensearch=unomi-base,unomi-startup,unomi-opensearch-core,unomi-persistence-core,unomi-services,unomi-rest-api,unomi-cxs-lists-extension,unomi-cxs-geonames-extension,unomi-cxs-privacy-extension,unomi-opensearch-conditions,unomi-plugins-base,unomi-plugins-request,unomi-plugins-mail,unomi-plugins-optimization-test,unomi-shell-dev-commands,unomi-wab,unomi-web-tracker,unomi-healthcheck,unomi-router-karaf-feature,unomi-groovy-actions,unomi-rest-ui,unomi-startup-complete,my-custom-plugin-opensearch" ]
+<features name="unomi-distributions" xmlns="http://karaf.apache.org/xmlns/features/v1.6.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.6.0 https://karaf.apache.org/xmlns/features/v1.6.0">
+    <feature name="unomi-distribution-custom" version="${project.version}">
+        <feature dependency="true" version="${project.version}">unomi-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-persistence-core</feature>
+        <feature dependency="true" version="${project.version}">unomi-services</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-api</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-lists-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-geonames-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-cxs-privacy-extension</feature>
+        <feature dependency="true" version="${project.version}">unomi-elasticsearch-conditions</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-base</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-request</feature>
+        <feature dependency="true" version="${project.version}">unomi-plugins-mail</feature>
+        <feature dependency="true" version="${project.version}">unomi-wab</feature>
+        <feature dependency="true" version="${project.version}">unomi-web-tracker</feature>
+        <feature dependency="true" version="${project.version}">unomi-healthcheck-elasticsearch</feature>
+        <feature dependency="true" version="${project.version}">unomi-router-karaf-feature</feature>
+        <feature dependency="true" version="${project.version}">unomi-groovy-actions</feature>
+        <feature dependency="true" version="${project.version}">unomi-rest-ui</feature>
+        <feature dependency="true" version="${project.version}">unomi-startup-complete</feature>
+        <feature dependency="true" version="${project.version}">custom-feature</feature>
+    </feature>
+</features>
 ----

 **Key Points:**
@@ -1008,14 +1053,14 @@ startFeatures = [ "elasticsearch=unomi-base,unomi-startup,unomi-elasticsearch-co
 - **Dependency management**: Ensure your plugin features are listed after their dependencies (e.g., after `unomi-elasticsearch-core` or `unomi-opensearch-core`)

-**Benefits of Custom Start Configuration:**
+**Benefits of Custom Distribution Feature:**

 - **Automatic deployment**: Your plugin is automatically installed when Apache Unomi starts
 - **Consistent environments**: Ensures the same features are deployed across all environments
 - **Production ready**: No manual feature installation required
 - **Version control**: Configuration can be versioned and managed with your deployment

-**Note**: For more detailed information about customizing start features configurations, including environment-specific examples, see the <<Customizing Start Features Configuration,Configuration>> section of the documentation.
+**Note**: For more detailed information about custom distributions, including environment-specific examples, see the <<Custom Distribution Feature,Configuration>> section of the documentation.

 6. **Migration from Legacy Implementations**
    - **DO NOT** use legacy mappings for custom query builders
diff --git c/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java i/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java
index 607f087..de76351 100644
--- c/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java
+++ i/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/UnomiManagementService.java
@@ -36,7 +36,7 @@ public interface UnomiManagementService {
     /**
      * This method will start Apache Unomi
      * @param mustStartFeatures true if features should be started, false if they should not
-     * @throws Exception if there was an error starting Unomi's bundles
+     * @throws Exception if there was an error starting Unomi's features
      */
     void startUnomi(boolean mustStartFeatures) throws Exception;

diff --git c/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java i/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java
index 5ae3499..8d4ecae 100644
--- c/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java
+++ i/tools/shell-commands/src/main/java/org/apache/unomi/shell/services/internal/UnomiManagementServiceImpl.java
@@ -43,28 +43,24 @@ import java.util.concurrent.*;
  *
  * <p>This service handles the following responsibilities:</p>
  * <ul>
- *   <li>Loading configuration from the OSGi Configuration Admin service, including start features configuration and feature lists.</li>
- *   <li>Starting Apache Unomi by installing and starting the configured features for a selected start features configuration.</li>
+ *   <li>Loading distribution's feature from an environment variable, a java option or by calling the setupUnomiDistribution method.</li>
+ *   <li>Starting Apache Unomi by installing and starting the configured features for a selected distribution.</li>
  *   <li>Stopping Apache Unomi by uninstalling features in reverse order to ensure proper teardown.</li>
  *   <li>Interfacing with the {@link org.apache.unomi.shell.migration.MigrationService} for migration tasks during startup.</li>
  * </ul>
  *
- * <p>The class is designed to be used within an OSGi environment and integrates with the Configuration Admin service
- * to dynamically adjust its behavior based on external configurations. It leverages the {@link FeaturesService} to
+ * <p>The class is designed to be used within an OSGi environment. It leverages the {@link FeaturesService} to
  * manage Karaf features dynamically.</p>
  *
  * <p><b>Configuration</b></p>
- * <p>The service reads its configuration from the OSGi Configuration Admin under the PID <code>org.apache.unomi.start</code>.
- * The configuration includes:</p>
- * <ul>
- *   <li><b>startFeatures</b>: A semicolon-separated list of features mapped to persistence implementations
- *       in the format <code>persistenceImplementation:feature1,feature2</code>.</li>
- * </ul>
+ * <p>The service stores its distribution's name using the OSGi Configuration Admin under the PID <code>org.apache.unomi.setup</code>.
+ * This allows the service to persist the selected distribution across restarts. The default distribution is unomi-distribution-elasticsearch</p>
  *
  * <p><b>Usage</b></p>
  * <p>This service can be controlled programmatically through its methods:</p>
  * <ul>
- *   <li>{@link #startUnomi(String, boolean)}: Installs and starts features for the specified start features configuration.</li>
+ *   <li>{@link #setupUnomiDistribution(String, boolean)}: Sets up the Unomi distribution's feature name.</li>
+ *   <li>{@link #startUnomi(boolean)}: Installs and starts features for the configured distribution.</li>
  *   <li>{@link #stopUnomi()}: Stops and uninstalls the previously started features.</li>
  * </ul>
  *
@jayblanc jayblanc changed the title UNOMI-919: Refactor the UNOMI startFeatures configuration to use a Karaf UNOMI-919: Refactor the UNOMI startFeatures configuration to use a Karaf feature Dec 31, 2025
@jayblanc jayblanc merged commit f9e9877 into apache:master Jan 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants