diff --git a/.gitignore b/.gitignore
index 51efbef39..0a580bcd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,7 @@ build/
 *.tgz
 
 /docker/*.override.yaml
+out/
+node_modules/
+.gradle
 /e2e-tests/allure-results/
diff --git a/api/Dockerfile b/api/Dockerfile
index adaa64009..738981ae2 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -17,7 +17,7 @@ RUN chown kafkaui /etc/kafkaui
 USER kafkaui
 
 ARG JAR_FILE
-COPY "/target/${JAR_FILE}" "/api.jar"
+COPY $JAR_FILE "/api.jar"
 
 ENV JAVA_OPTS=
 
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 000000000..f2d2fc120
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,133 @@
+
+plugins {
+    id 'antlr'
+    id 'checkstyle'
+    alias(libs.plugins.spring.boot)
+    alias(libs.plugins.git.properties)
+    alias(libs.plugins.docker.remote.api)
+    alias(libs.plugins.spring.dependency.management)
+}
+
+import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage
+
+dependencies {
+    implementation project(":contract")
+    if (prod) {
+        implementation project(":frontend")
+    }
+    implementation project(":serde-api")
+    implementation libs.spring.starter.webflux
+    implementation libs.spring.starter.security
+    implementation libs.spring.starter.actuator
+    implementation libs.spring.starter.logging
+    implementation libs.spring.starter.oauth2.client
+    implementation libs.spring.boot.actuator
+    compileOnly libs.spring.boot.devtools
+
+    implementation libs.spring.security.ldap
+
+    implementation libs.kafka.clients
+
+    implementation libs.apache.avro
+    implementation libs.apache.commons
+    implementation libs.apache.commons.pool2
+    implementation libs.apache.datasketches
+
+    implementation libs.confluent.schema.registry.client
+    implementation libs.confluent.avro.serializer
+    implementation libs.confluent.protobuf.serializer
+    implementation libs.confluent.json.schema.serializer
+
+    implementation libs.aws.msk.auth
+    implementation (libs.azure.identity) {
+        exclude group: 'io.netty', module: 'netty-tcnative-boringssl-static'
+    }
+
+    implementation libs.jackson.databind.nullable
+    implementation libs.cel
+    antlr libs.antlr
+    implementation libs.antlr.runtime
+
+    implementation libs.opendatadiscovery.oddrn
+    implementation (libs.opendatadiscovery.client) {
+        exclude group: 'org.springframework.boot', module: 'spring-boot-starter-webflux'
+        exclude group: 'io.projectreactor', module: 'reactor-core'
+        exclude group: 'io.projectreactor.ipc', module: 'reactor-netty'
+    }
+
+    runtimeOnly libs.micrometer.registry.prometheus
+
+    // CVE Fixes
+    implementation libs.apache.commons.compress
+    implementation libs.okhttp3.logging.intercepter
+
+    // Annotation processors
+    implementation libs.lombok
+    implementation libs.mapstruct
+    annotationProcessor libs.lombok
+    annotationProcessor libs.mapstruct.processor
+    annotationProcessor libs.spring.boot.configuration.processor
+    testAnnotationProcessor libs.lombok
+    testAnnotationProcessor libs.mapstruct.processor
+
+    // Tests
+    testImplementation libs.spring.starter.test
+    testImplementation libs.reactor.test
+    testImplementation libs.testcontainers
+    testImplementation libs.testcontainers.kafka
+    testImplementation libs.testcontainers.jupiter
+    testImplementation libs.junit.jupiter.engine
+    testImplementation libs.mockito.core
+    testImplementation libs.mockito.jupiter
+    testImplementation libs.bytebuddy
+    testImplementation libs.assertj
+    testImplementation libs.jsonschemavalidator
+
+    testImplementation libs.okhttp3
+    testImplementation libs.okhttp3.mockwebserver
+}
+
+generateGrammarSource {
+    maxHeapSize = "64m"
+    arguments += ["-package", "ksql"]
+}
+
+sourceSets {
+    main {
+        antlr {
+            srcDirs = ["src/main/antlr4"]
+        }
+        java {
+            srcDirs += generateGrammarSource.outputDirectory
+        }
+    }
+}
+
+tasks.withType(Checkstyle).configureEach {
+    exclude '**/ksql/**'
+}
+
+checkstyle {
+    toolVersion = libs.versions.checkstyle.get()
+    configFile = rootProject.file('etc/checkstyle/checkstyle.xml')
+    ignoreFailures = false
+    maxWarnings = 0
+    maxErrors = 0
+}
+
+test {
+    useJUnitPlatform()
+}
+
+tasks.register('buildDockerImage', DockerBuildImage) {
+    inputDir = projectDir
+    dockerFile = project.layout.projectDirectory.file('Dockerfile')
+    buildArgs = [
+       'JAR_FILE': "build/libs/${project.name}-${project.version}.jar"
+    ] as Map<String, String>
+    images.add("ghcr.io/kafbat/kafka-ui:${project.version}")
+}
+
+if (prod) {
+    tasks.build.finalizedBy buildDockerImage
+}
diff --git a/api/pom.xml b/api/pom.xml
index bbeb9dff8..14a15c1df 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -443,10 +443,10 @@
                                     <goal>copy-resources</goal>
                                 </goals>
                                 <configuration>
-                                    <outputDirectory>${basedir}/target/classes/static</outputDirectory>
+                                    <outputDirectory>${basedir}/target/classes/</outputDirectory>
                                     <resources>
                                         <resource>
-                                            <directory>../frontend/build</directory>
+                                            <directory>../frontend/build/vite/static</directory>
                                         </resource>
                                     </resources>
                                 </configuration>
@@ -509,7 +509,7 @@
                                     <build>
                                         <contextDir>${project.basedir}</contextDir>
                                         <args>
-                                            <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
+                                            <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
                                         </args>
                                     </build>
                                 </image>
diff --git a/api/src/main/java/io/kafbat/ui/config/WebclientProperties.java b/api/src/main/java/io/kafbat/ui/config/WebclientProperties.java
index afe0fa8ea..8b147c898 100644
--- a/api/src/main/java/io/kafbat/ui/config/WebclientProperties.java
+++ b/api/src/main/java/io/kafbat/ui/config/WebclientProperties.java
@@ -1,7 +1,7 @@
 package io.kafbat.ui.config;
 
 import io.kafbat.ui.exception.ValidationException;
-import javax.annotation.PostConstruct;
+import jakarta.annotation.PostConstruct;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
diff --git a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java
index ee53d4424..8ecf12b99 100644
--- a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java
+++ b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java
@@ -1,9 +1,9 @@
 package io.kafbat.ui.config.auth;
 
 import io.kafbat.ui.model.rbac.Role;
+import jakarta.annotation.PostConstruct;
 import java.util.ArrayList;
 import java.util.List;
-import javax.annotation.PostConstruct;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
 @ConfigurationProperties("rbac")
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..314805940
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,46 @@
+plugins {
+    alias(libs.plugins.nexus.publish.plugin)
+}
+
+subprojects {
+    apply plugin: "java"
+
+    repositories {
+        mavenCentral()
+        maven {
+            name = 'confluent'
+            url = 'https://packages.confluent.io/maven/'
+        }
+    }
+
+    group = 'io.kafbat'
+    version = version
+
+    java {
+        sourceCompatibility = JavaVersion.VERSION_21
+        targetCompatibility = JavaVersion.VERSION_21
+    }
+}
+
+boolean resolveBooleanProperty(String propertyName, boolean defaultValue = false) {
+    def propertyValueStr = findProperty(propertyName)
+    return propertyValueStr == null ? defaultValue : propertyValueStr.toBoolean();
+}
+
+ext {
+    prod = resolveBooleanProperty("prod")
+}
+
+if (prod) { // TODO shouldn't be prod, 'publish' instead?
+    nexusPublishing {
+        repositories {
+            sonatype {
+                nexusUrl = uri("https://s01.oss.sonatype.org/service/local/")
+                snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+
+                username = sonatypeUsername
+                password = sonatypePassword
+            }
+        }
+    }
+}
diff --git a/contract/build.gradle b/contract/build.gradle
new file mode 100644
index 000000000..6aba087a9
--- /dev/null
+++ b/contract/build.gradle
@@ -0,0 +1,114 @@
+import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
+
+plugins {
+    id "java-library"
+    alias(libs.plugins.openapi.generator)
+}
+
+
+def specDir = project.layout.projectDirectory.dir("src/main/resources/swagger/")
+def targetDir = project.layout.buildDirectory.dir("generated").get()
+
+dependencies {
+    implementation libs.spring.starter.webflux
+    implementation libs.spring.starter.validation
+    api libs.swagger.integration.jakarta
+    api libs.jackson.databind.nullable
+    api libs.jakarta.annotation.api
+    compileOnly libs.lombok
+    annotationProcessor libs.lombok
+}
+
+tasks.register('generateUiClient', GenerateTask) {
+    generatorName = "java"
+    inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath
+    outputDir = targetDir.dir("kafbat-ui-client").asFile.absolutePath
+    apiPackage = "io.kafbat.ui.api.api"
+    invokerPackage = "io.kafbat.ui.api"
+    modelPackage = "io.kafbat.ui.api.model"
+
+    configOptions = [library          : 'webclient',
+                     asyncNative      : 'true',
+                     useBeanValidation: 'true',
+                     dateLibrary      : 'java8',
+                     useJakartaEe     : 'true',]
+}
+
+tasks.register('generateBackendApi', GenerateTask) {
+    generatorName = "spring"
+    inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath
+    outputDir = targetDir.dir("api").asFile.absolutePath
+    apiPackage = "io.kafbat.ui.api"
+    invokerPackage = "io.kafbat.ui.api"
+    modelPackage = "io.kafbat.ui.model"
+    modelNameSuffix = "DTO"
+
+    additionalProperties = [removeEnumValuePrefix: "false"]
+
+    configOptions = [reactive                            : "true",
+                     interfaceOnly                       : "true",
+                     skipDefaultInterface                : "true",
+                     useTags                             : "true",
+                     useSpringBoot3                      : "true",
+                     dateLibrary                         : "java8",
+                     generateConstructorWithAllArgs      : "false",
+                     generatedConstructorWithRequiredArgs: "false",
+                     additionalModelTypeAnnotations      : """
+            @lombok.experimental.SuperBuilder
+            @lombok.AllArgsConstructor
+            @lombok.NoArgsConstructor
+            """]
+}
+
+tasks.register('generateConnectClient', GenerateTask) {
+    generatorName = "java"
+    inputSpec = specDir.file("kafka-connect-api.yaml").asFile.absolutePath
+    outputDir = targetDir.dir("kafka-connect-client").asFile.absolutePath
+    generateApiTests = false
+    generateModelTests = false
+    apiPackage = "io.kafbat.ui.connect.api"
+    modelPackage = "io.kafbat.ui.connect.model"
+    invokerPackage = "io.kafbat.ui.connect"
+
+
+    configOptions = [asyncNative      : "true",
+                     library          : "webclient",
+                     useJakartaEe     : "true",
+                     useBeanValidation: "true",
+                     dateLibrary      : "java8",]
+}
+
+tasks.register('generateSchemaRegistryClient', GenerateTask) {
+    generatorName = "java"
+    inputSpec = specDir.file("kafka-sr-api.yaml").asFile.absolutePath
+    outputDir = targetDir.dir("kafka-sr-client").asFile.absolutePath
+    generateApiTests = false
+    generateModelTests = false
+    apiPackage = "io.kafbat.ui.sr.api"
+    invokerPackage = "io.kafbat.ui.sr"
+    modelPackage = "io.kafbat.ui.sr.model"
+
+    configOptions = [asyncNative      : "true",
+                     library          : "webclient",
+                     useJakartaEe     : "true",
+                     useBeanValidation: "true",
+                     dateLibrary      : "java8",]
+}
+
+sourceSets {
+    main {
+        java {
+            srcDir targetDir.dir("api/src/main/java")
+            srcDir targetDir.dir("kafka-connect-client/src/main/java")
+            srcDir targetDir.dir("kafbat-ui-client/src/main/java")
+            srcDir targetDir.dir("kafka-sr-client/src/main/java")
+        }
+
+        resources {
+            srcDir specDir
+        }
+    }
+}
+
+compileJava.dependsOn generateUiClient, generateBackendApi, generateConnectClient, generateSchemaRegistryClient
+processResources.dependsOn generateUiClient, generateBackendApi, generateConnectClient, generateSchemaRegistryClient
diff --git a/e2e-tests/build.gradle b/e2e-tests/build.gradle
new file mode 100644
index 000000000..6638102e4
--- /dev/null
+++ b/e2e-tests/build.gradle
@@ -0,0 +1,41 @@
+plugins {
+    id 'java'
+    id 'checkstyle'
+    alias(libs.plugins.allure)
+}
+
+dependencies {
+    implementation project(":contract")
+    implementation libs.apache.kafka
+    implementation libs.apache.commons.io
+    implementation libs.aspectj
+    implementation libs.testng
+    implementation libs.codeborne.selenide
+    implementation libs.allure.testng
+    implementation libs.allure.selenide
+    implementation libs.bonigarcia.webdrivermanager
+    implementation libs.spring.starter.webflux
+    implementation libs.netty.resolver.dns.native
+
+    implementation libs.lombok
+
+    annotationProcessor libs.lombok
+    testAnnotationProcessor libs.lombok
+}
+
+checkstyle {
+    toolVersion = libs.versions.checkstyle.get()
+    configFile = rootProject.file('etc/checkstyle/checkstyle-e2e.xml')
+    ignoreFailures = false
+    maxWarnings = 0
+    maxErrors = 0
+}
+
+test {
+    useTestNG()
+}
+
+tasks.named('test') {
+    enabled = prod
+}
+
diff --git a/frontend/build.gradle b/frontend/build.gradle
new file mode 100644
index 000000000..cd93fa7ad
--- /dev/null
+++ b/frontend/build.gradle
@@ -0,0 +1,46 @@
+plugins {
+  alias(libs.plugins.node.gradle)
+}
+
+node {
+  download = "true" != project.property("local_node")
+  version = project.property("node_version").toString()
+  pnpmVersion = project.property("pnpm_version").toString()
+  nodeProjectDir = project.layout.projectDirectory
+}
+
+
+tasks.named("pnpmInstall") {
+  inputs.files(file("package.json"))
+  outputs.dir(project.layout.projectDirectory.dir("node_modules"))
+}
+
+tasks.register('generateContract', PnpmTask) {
+  dependsOn pnpmInstall
+  inputs.files(fileTree("../contract/src/main/resources"))
+  outputs.dir(project.layout.projectDirectory.dir("src/generated-sources"))
+  args = ['gen:sources']
+}
+
+tasks.register('buildFrontend', PnpmTask) {
+  dependsOn generateContract
+  inputs.files(fileTree("src/"))
+  outputs.dir(project.layout.buildDirectory.dir("vite"))
+  args = ['build']
+  environment = System.getenv() + [
+    "VITE_TAG": project.version,
+    // add git commit
+    "VITE_COMMIT": ""
+  ]
+}
+
+sourceSets {
+  main {
+    resources {
+      srcDir "build/vite"
+    }
+  }
+}
+
+processResources.dependsOn buildFrontend
+compileJava.dependsOn buildFrontend
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..1d90b7b43
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,7 @@
+description='Kafbat UI'
+version=0.0.1-SNAPSHOT
+group=io.kafbat.ui
+local_node = false
+node_version = 22.12.0
+pnpm_version = 9.15.0
+tags=[]
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..a5c0e4bde
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,122 @@
+[versions]
+spring-boot = '3.4.1'
+
+aws-msk-auth = '2.2.0'
+azure-identity = '1.14.2'
+
+apache-commons-lang3 = '3.12.0'
+apache-commons-io = '2.16.1'
+apache-commons-pool2 = '2.12.0'
+apache-datasketches = '3.1.0'
+apache-commons-compress = '1.26.0'
+
+assertj = '3.25.3'
+avro = '1.11.4'
+byte-buddy = '1.14.19'
+confluent = '7.8.0'
+confluent-ccs = '7.8.0-ccs'
+
+mapstruct = '1.6.2'
+lombok = '1.18.34'
+odd-oddrn-generator = '0.1.17'
+odd-oddrn-client = '0.1.39'
+cel = '0.3.0'
+junit = '5.11.2'
+mockito = '5.14.2'
+okhttp3 = '4.12.0'
+testcontainers = '1.20.4'
+swagger-integration-jakarta = '2.2.8'
+jakarta-annotation-api = '2.1.1'
+jackson-databind-nullable = '0.2.6'
+antlr = '4.12.0'
+json-schema-validator = '2.2.14'
+checkstyle = '10.3.1'
+
+allure = '2.27.0'
+selenide = '7.2.3'
+testng = '7.10.0'
+bonigarcia-webdrivermanager = '5.8.0'
+aspectj = '1.9.21'
+
+[plugins]
+spring-boot = { id = 'org.springframework.boot', version.ref = 'spring-boot' }
+spring-dependency-management = { id = 'io.spring.dependency-management', version = '1.1.3' }
+git-properties = { id = 'com.gorylenko.gradle-git-properties', version = '2.4.2' }
+openapi-generator = { id = 'org.openapi.generator', version = '7.9.0' }
+allure = { id = 'io.qameta.allure', version = '2.10.0' }
+nexus-publish-plugin = { id = 'io.github.gradle-nexus.publish-plugin', version = '1.1.0' }
+node-gradle = { id = 'com.github.node-gradle.node', version = '7.0.2' }
+#jib = { id = 'com.google.cloud.tools.jib', version = '3.4.4' }
+docker-remote-api = { id = 'com.bmuschko.docker-remote-api', version = '9.4.0' }
+
+[libraries]
+spring-starter-actuator = { module = 'org.springframework.boot:spring-boot-starter-actuator', version.ref = 'spring-boot' }
+spring-starter-test = { module = 'org.springframework.boot:spring-boot-starter-test', version.ref = 'spring-boot' }
+spring-starter-webflux = { module = 'org.springframework.boot:spring-boot-starter-webflux', version.ref = 'spring-boot' }
+spring-starter-security = { module = 'org.springframework.boot:spring-boot-starter-security', version.ref = 'spring-boot' }
+spring-starter-validation = { module = 'org.springframework.boot:spring-boot-starter-validation', version.ref = 'spring-boot' }
+spring-starter-oauth2-client = { module = 'org.springframework.boot:spring-boot-starter-oauth2-client', version.ref = 'spring-boot' }
+spring-starter-logging = { module = 'org.springframework.boot:spring-boot-starter-logging', version.ref = 'spring-boot' }
+spring-boot-actuator = { module = 'org.springframework.boot:spring-boot-actuator', version.ref = 'spring-boot' }
+spring-boot-devtools = { module = 'org.springframework.boot:spring-boot-devtools', version.ref = 'spring-boot' }
+spring-boot-configuration-processor = { module = 'org.springframework.boot:spring-boot-configuration-processor', version.ref = 'spring-boot' }
+
+spring-security-ldap = { module = 'org.springframework.security:spring-security-ldap' }
+
+swagger-integration-jakarta = { module = 'io.swagger.core.v3:swagger-integration-jakarta', version.ref = 'swagger-integration-jakarta' }
+lombok = { module = 'org.projectlombok:lombok', version.ref = 'lombok' }
+mapstruct = { module = 'org.mapstruct:mapstruct', version.ref = 'mapstruct' }
+mapstruct-processor = { module = 'org.mapstruct:mapstruct-processor', version.ref = 'mapstruct' }
+jakarta-annotation-api = { module = 'jakarta.annotation:jakarta.annotation-api', version.ref = 'jakarta-annotation-api' }
+jackson-databind-nullable = { module = 'org.openapitools:jackson-databind-nullable', version.ref = 'jackson-databind-nullable' }
+kafka-clients = { module = 'org.apache.kafka:kafka-clients', version.ref = 'confluent-ccs' }
+
+apache-commons = { module = 'org.apache.commons:commons-lang3', version.ref = 'apache-commons-lang3' }
+apache-commons-compress = { module = 'org.apache.commons:commons-compress', version.ref = 'apache-commons-compress' }
+apache-commons-io = { module = 'commons-io:commons-io', version.ref = 'apache-commons-io' }
+apache-commons-pool2 = { module = 'org.apache.commons:commons-pool2', version.ref = 'apache-commons-pool2' }
+apache-datasketches = { module = 'org.apache.datasketches:datasketches-java', version.ref = 'apache-datasketches' }
+apache-avro = { module = 'org.apache.avro:avro', version.ref = 'avro' }
+apache-kafka = { module = 'org.apache.kafka:kafka_2.13', version.ref = 'confluent-ccs' }
+
+confluent-schema-registry-client = { module = 'io.confluent:kafka-schema-registry-client', version.ref = 'confluent' }
+confluent-avro-serializer = { module = 'io.confluent:kafka-avro-serializer', version.ref = 'confluent' }
+confluent-protobuf-serializer = { module = 'io.confluent:kafka-protobuf-serializer', version.ref = 'confluent' }
+confluent-json-schema-serializer = { module = 'io.confluent:kafka-json-schema-serializer', version.ref = 'confluent' }
+
+aws-msk-auth = { module = 'software.amazon.msk:aws-msk-iam-auth', version.ref = 'aws-msk-auth' }
+azure-identity = { module = 'com.azure:azure-identity', version.ref = 'azure-identity' }
+reactor-test = { module = 'io.projectreactor:reactor-test' }
+micrometer-registry-prometheus = { module = 'io.micrometer:micrometer-registry-prometheus' }
+antlr = { module = 'org.antlr:antlr4', version.ref = 'antlr' }
+antlr-runtime = { module = 'org.antlr:antlr4-runtime', version.ref = 'antlr' }
+cel = { module = 'dev.cel:cel', version.ref = 'cel' }
+
+testcontainers = { module = 'org.testcontainers:testcontainers', version.ref = 'testcontainers' }
+testcontainers-kafka = { module = 'org.testcontainers:kafka', version.ref = 'testcontainers' }
+testcontainers-jupiter = { module = 'org.testcontainers:junit-jupiter', version.ref = 'testcontainers' }
+
+junit-jupiter-engine = { module = 'org.junit.jupiter:junit-jupiter-engine', version.ref = 'junit' }
+
+mockito-core = { module = 'org.mockito:mockito-core', version.ref = 'mockito' }
+mockito-jupiter = { module = 'org.mockito:mockito-junit-jupiter', version.ref = 'mockito' }
+
+okhttp3 = { module = 'com.squareup.okhttp3:okhttp', version.ref = 'okhttp3' }
+okhttp3-mockwebserver = { module = 'com.squareup.okhttp3:mockwebserver', version.ref = 'okhttp3' }
+okhttp3-logging-intercepter = { module = 'com.squareup.okhttp3:logging-interceptor', version.ref = 'okhttp3' }
+
+opendatadiscovery-oddrn = { module = 'org.opendatadiscovery:oddrn-generator-java', version.ref = 'odd-oddrn-generator' }
+opendatadiscovery-client = { module = 'org.opendatadiscovery:ingestion-contract-client', version.ref = 'odd-oddrn-client' }
+
+bytebuddy = { module = 'net.bytebuddy:byte-buddy', version.ref = 'byte-buddy' }
+assertj = { module = 'org.assertj:assertj-core', version.ref = 'assertj' }
+jsonschemavalidator = { module = 'com.github.java-json-tools:json-schema-validator', version.ref = 'json-schema-validator' }
+
+allure-testng = { module = 'io.qameta.allure:allure-testng', version.ref = 'allure' }
+allure-selenide = { module = 'io.qameta.allure:allure-selenide', version.ref = 'allure' }
+
+codeborne-selenide = { module = 'com.codeborne:selenide', version.ref = 'selenide' }
+testng = { module = 'org.testng:testng', version.ref = 'testng' }
+aspectj = { module = 'org.aspectj:aspectjweaver', version.ref = 'aspectj' }
+bonigarcia-webdrivermanager = { module = 'io.github.bonigarcia:webdrivermanager', version.ref = 'bonigarcia-webdrivermanager' }
+netty-resolver-dns-native = { module = 'io.netty:netty-resolver-dns-native-macos' }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e6441136f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..cea7a793a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..1aa94a426
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed 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
+#
+#      https://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.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..25da30dbd
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/serde-api/build.gradle b/serde-api/build.gradle
new file mode 100644
index 000000000..a909f4e87
--- /dev/null
+++ b/serde-api/build.gradle
@@ -0,0 +1,86 @@
+plugins {
+    id 'java-library'
+    id 'signing'
+    id 'maven-publish'
+}
+
+tasks.register('sourceJar', Jar) {
+    archiveClassifier.set("sources")
+    from sourceSets.main.allJava
+}
+
+tasks.register('javadocJar', Jar) {
+    dependsOn javadoc
+    archiveClassifier.set("javadoc")
+    from javadoc.destinationDir
+}
+
+artifacts {
+    archives sourceJar, javadocJar
+}
+
+if (prod) { // TODO shouldn't be prod, 'publish' instead?
+    signing {
+        sign(publishing.publications)
+    }
+}
+
+publishing {
+    if (prod) { // TODO shouldn't be prod, 'publish' instead?
+        repositories {
+            maven {
+                url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2"
+                credentials {
+                    username sonatypeUsername
+                    password sonatypePassword
+                }
+            }
+        }
+    }
+
+    publications {
+        maven(MavenPublication) {
+            groupId = 'io.kafbat.ui'
+            artifactId = 'serde-api'
+            version = version
+
+            from components.java
+
+            artifact(sourceJar) {
+                classifier = 'sources'
+            }
+
+            artifact(javadocJar) {
+                classifier = 'javadoc'
+            }
+
+            pom {
+                name = 'kafbat-ui-serde-api'
+                description = 'kafbat-ui-serde-api'
+                url = 'http://github.com/kafbat/kafka-ui'
+
+                licenses {
+                    license {
+                        name = 'The Apache License, Version 2.0'
+                        url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
+                    }
+                }
+
+                developers {
+                    developer {
+                        id = 'Kafbat'
+                        name = 'Kafbat'
+                        organization = 'Kafbat'
+                        email = 'maintainers@kafbat.io'
+                    }
+                }
+
+                scm {
+                    connection = 'scm:git://github.com/kafbat/kafka-ui.git'
+                    developerConnection = 'scm:git:ssh://github.com:kafbat/kafka-ui.git'
+                    url = 'https://github.com/kafbat/kafka-ui'
+                }
+            }
+        }
+    }
+}
diff --git a/serde-api/gradle.properties b/serde-api/gradle.properties
new file mode 100644
index 000000000..8577ce8c4
--- /dev/null
+++ b/serde-api/gradle.properties
@@ -0,0 +1,2 @@
+sonatypeUsername=
+sonatypePassword=
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..a9be51116
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,14 @@
+pluginManagement {
+    repositories {
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+
+rootProject.name = "kafbat-ui"
+include "contract"
+include "serde-api"
+include "api"
+include "frontend"
+include "e2e-tests"
+