diff --git a/.github/workflows/native-smoke.yml b/.github/workflows/native-smoke.yml
new file mode 100644
index 0000000..7f7fac3
--- /dev/null
+++ b/.github/workflows/native-smoke.yml
@@ -0,0 +1,110 @@
+name: Native Image Smoke Test
+
+# Builds the observability-kit-starter test application as a GraalVM native
+# image and runs the existing BootMetricsIT browser test against the native
+# server. This verifies the starter survives AOT / closed-world compilation:
+# the ServiceLoader-discovered MetricsServiceInitListener must still be
+# instantiated and the vaadin.* meters must still reach the Prometheus endpoint.
+#
+# Native builds are slow (~10-20 min) and need GraalVM, so this does not run on
+# every push. It runs nightly, on demand, and on PRs that touch the kit sources
+# or the native build wiring.
+
+on:
+ workflow_dispatch:
+ schedule:
+ # 03:17 UTC nightly — off the top of the hour to avoid scheduler congestion.
+ - cron: '17 3 * * *'
+ pull_request:
+ paths:
+ - 'observability-kit-micrometer/src/**'
+ - 'observability-kit-spring/src/**'
+ - 'observability-kit-starter/src/**'
+ - 'observability-kit-tests/observability-kit-tests-starter/**'
+ - '.github/workflows/native-smoke.yml'
+
+concurrency:
+ group: native-smoke-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ native-smoke:
+ name: Build native image and verify metrics
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check secrets
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ run: |
+ [ -z "${{secrets.VAADIN_PRO_KEY}}" ] \
+ && echo "🚫 **VAADIN_PRO_KEY** is not defined, check that **${{github.repository}}** repo has a valid secret" \
+ | tee -a $GITHUB_STEP_SUMMARY && exit 1 || exit 0
+
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up GraalVM 25
+ uses: graalvm/setup-graalvm@v1
+ with:
+ distribution: graalvm
+ java-version: '25'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ cache: maven
+
+ - name: Set TB License
+ run: |
+ TB_LICENSE=${{secrets.VAADIN_PRO_KEY}}
+ mkdir -p ~/.vaadin/
+ echo '{"username":"'$(echo $TB_LICENSE | cut -d / -f1)'","proKey":"'$(echo $TB_LICENSE | cut -d / -f2)'"}' > ~/.vaadin/proKey
+
+ # Install the kit modules (micrometer, spring, starter) to the local repo.
+ # skipITs keeps the observability-kit-tests reactor out of this build.
+ - name: Install kit modules
+ run: mvn -B -ntp -DskipTests -DskipITs install -pl observability-kit-starter -am
+
+ # Build the test app as a native image (production frontend + Spring AOT +
+ # native-image). `package` stops before the integration-test phase, so the
+ # JVM start/stop executions do not fire here.
+ - name: Build native image
+ run: >
+ mvn -B -ntp -Pproduction,native -DskipTests package
+ -pl observability-kit-tests/observability-kit-tests-common,observability-kit-tests/observability-kit-tests-starter
+
+ - name: Start native server
+ working-directory: observability-kit-tests/observability-kit-tests-starter
+ run: |
+ ./target/observability-kit-tests-starter \
+ -Dvaadin.productionMode=true --server.port=8080 \
+ > native-server.log 2>&1 &
+ echo "NATIVE_PID=$!" >> "$GITHUB_ENV"
+
+ - name: Wait for server
+ run: |
+ for i in $(seq 1 60); do
+ if curl -sf http://localhost:8080/actuator/prometheus > /dev/null; then
+ echo "Native server is up after ${i}s"
+ exit 0
+ fi
+ sleep 1
+ done
+ echo "🚫 Native server did not become ready within 60s"
+ exit 1
+
+ # Run the existing browser IT against the native server. Failsafe is
+ # invoked directly so it uses the already-running native binary instead of
+ # booting the JVM app via the start/stop executions.
+ - name: Run smoke test against native server
+ run: >
+ mvn -B -ntp -pl observability-kit-tests/observability-kit-tests-starter
+ failsafe:integration-test failsafe:verify -DserverPort=8080
+
+ - name: Stop native server
+ if: always()
+ run: '[ -n "$NATIVE_PID" ] && kill "$NATIVE_PID" || true'
+
+ - name: Upload native server log
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: native-server-log
+ path: observability-kit-tests/observability-kit-tests-starter/native-server.log
+ if-no-files-found: ignore
diff --git a/observability-kit-tests/observability-kit-tests-starter/pom.xml b/observability-kit-tests/observability-kit-tests-starter/pom.xml
index 3d00266..8655346 100644
--- a/observability-kit-tests/observability-kit-tests-starter/pom.xml
+++ b/observability-kit-tests/observability-kit-tests-starter/pom.xml
@@ -38,22 +38,13 @@
${flow.version}
-
+
org.springframework.boot
spring-boot-starter-webmvc
-
-
- org.springframework.boot
- spring-boot-starter-tomcat
-
-
-
-
- org.springframework.boot
- spring-boot-starter-jetty
-
+
+
org.springframework.boot
@@ -160,4 +151,91 @@
+
+
+
+ native
+
+ 0.10.6
+
+
+
+
+
+ com.vaadin
+ flow-maven-plugin
+
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ process-aot
+
+ process-aot
+
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ ${native-maven-plugin.version}
+
+ ${project.build.outputDirectory}
+ observability-kit-tests-starter
+ com.vaadin.observability.tests.starter.Application
+
+
+ true
+
+
+
+
+ add-reachability-metadata
+
+ add-reachability-metadata
+
+
+
+ build-native-image
+ package
+
+ compile-no-fork
+
+
+
+
+
+
+
+
+