diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 103609a56..151774ae6 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -64,6 +64,8 @@ dependencies {
implementation(project(":core:domain"))
implementation(project(":core:data"))
implementation(project(":core:ui"))
+ implementation(project(":backend:api"))
+ implementation(project(":backend:config"))
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
diff --git a/android/backend/config/build.gradle.kts b/android/backend/config/build.gradle.kts
new file mode 100644
index 000000000..3e0090952
--- /dev/null
+++ b/android/backend/config/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.serialization)
+}
+
+android {
+ namespace = "io.clawdroid.backend.config"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation(project(":backend:api"))
+ implementation(project(":core:ui"))
+
+ implementation(libs.ktor.client.okhttp)
+ implementation(libs.ktor.client.content.negotiation)
+ implementation(libs.ktor.serialization.json)
+ implementation(libs.serialization.json)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.compose.ui)
+ implementation(libs.compose.material3)
+ implementation(libs.navigation.compose)
+
+ implementation(libs.koin.android)
+ implementation(libs.koin.compose)
+
+ implementation(libs.coroutines.android)
+
+ debugImplementation(libs.compose.ui.tooling)
+}
diff --git a/android/backend/config/src/main/AndroidManifest.xml b/android/backend/config/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..8072ee00d
--- /dev/null
+++ b/android/backend/config/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt
new file mode 100644
index 000000000..809cb944b
--- /dev/null
+++ b/android/backend/config/src/main/java/io/clawdroid/backend/config/ConfigApiClient.kt
@@ -0,0 +1,79 @@
+package io.clawdroid.backend.config
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.request.get
+import io.ktor.client.request.put
+import io.ktor.client.request.setBody
+import io.ktor.client.statement.HttpResponse
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
+import io.ktor.http.isSuccess
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import java.io.Closeable
+import java.io.IOException
+
+@Serializable
+data class ConfigSchema(val sections: List)
+
+@Serializable
+data class SchemaSection(val key: String, val label: String, val fields: List)
+
+@Serializable
+data class SchemaField(
+ val key: String,
+ val label: String,
+ val type: String,
+ val secret: Boolean = false,
+ val default: JsonElement = Json.parseToJsonElement("null"),
+)
+
+@Serializable
+data class SaveConfigResult(
+ val status: String? = null,
+ val restart: Boolean = false,
+ val error: String? = null,
+)
+
+class ConfigApiClient : Closeable {
+ private val baseUrl: String get() = "http://127.0.0.1:18790"
+
+ private val client = HttpClient(OkHttp) {
+ install(ContentNegotiation) {
+ json(Json { ignoreUnknownKeys = true; isLenient = true })
+ }
+ }
+
+ suspend fun getSchema(): ConfigSchema {
+ return client.get("$baseUrl/api/config/schema").ensureSuccess().body()
+ }
+
+ suspend fun getConfig(): JsonObject {
+ return client.get("$baseUrl/api/config").ensureSuccess().body()
+ }
+
+ suspend fun saveConfig(config: JsonObject): SaveConfigResult {
+ return client.put("$baseUrl/api/config") {
+ contentType(ContentType.Application.Json)
+ setBody(config)
+ }.ensureSuccess().body()
+ }
+
+ override fun close() {
+ client.close()
+ }
+
+ private suspend fun HttpResponse.ensureSuccess(): HttpResponse {
+ if (!status.isSuccess()) {
+ val error = runCatching { body().error }.getOrNull()
+ throw IOException("HTTP ${status.value}: ${error ?: "request failed"}")
+ }
+ return this
+ }
+}
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index c8eb14336..70700201d 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -19,3 +19,4 @@ include(":core:domain")
include(":core:data")
include(":core:ui")
include(":backend:api")
+include(":backend:config")