diff --git a/cameleer3-server-app/pom.xml b/cameleer3-server-app/pom.xml
index 891d4762..9ef6d3e8 100644
--- a/cameleer3-server-app/pom.xml
+++ b/cameleer3-server-app/pom.xml
@@ -66,6 +66,10 @@
org.eclipse.xtext.xbase.lib
2.37.0
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.springframework.boot
spring-boot-starter-security
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/OpenApiConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/OpenApiConfig.java
new file mode 100644
index 00000000..93fe69ba
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/OpenApiConfig.java
@@ -0,0 +1,90 @@
+package com.cameleer3.server.app.config;
+
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import org.springdoc.core.customizers.OpenApiCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@Configuration
+@SecurityScheme(name = "bearer", type = SecuritySchemeType.HTTP,
+ scheme = "bearer", bearerFormat = "JWT")
+public class OpenApiConfig {
+
+ /**
+ * Core domain models that always have all fields populated.
+ * Mark all their properties as required so the generated TypeScript
+ * types are non-optional.
+ */
+ private static final Set ALL_FIELDS_REQUIRED = Set.of(
+ "ExecutionSummary", "ExecutionDetail", "ExecutionStats",
+ "StatsTimeseries", "TimeseriesBucket",
+ "SearchResultExecutionSummary", "UserInfo",
+ "ProcessorNode"
+ );
+
+ @Bean
+ public OpenAPI openAPI() {
+ return new OpenAPI()
+ .info(new Info().title("Cameleer3 Server API").version("1.0"))
+ .addSecurityItem(new SecurityRequirement().addList("bearer"))
+ .servers(List.of());
+ }
+
+ @Bean
+ public OpenApiCustomizer pathPrefixStripper() {
+ return openApi -> {
+ var original = openApi.getPaths();
+ if (original == null) return;
+ String prefix = "/api/v1";
+ var stripped = new Paths();
+ for (var entry : original.entrySet()) {
+ String path = entry.getKey();
+ stripped.addPathItem(
+ path.startsWith(prefix) ? path.substring(prefix.length()) : path,
+ entry.getValue());
+ }
+ openApi.setPaths(stripped);
+ };
+ }
+
+ @Bean
+ @SuppressWarnings("unchecked")
+ public OpenApiCustomizer schemaCustomizer() {
+ return openApi -> {
+ var schemas = openApi.getComponents().getSchemas();
+ if (schemas == null) return;
+
+ // Add children to ProcessorNode if missing (recursive self-reference)
+ if (schemas.containsKey("ProcessorNode")) {
+ Schema