Kotlin Multiplatform SDK and Worker gateway for Cloudflare — a type-safe, coroutine-first client for D1, KV, R2, and realtime-style app backends.
Cloudflare gives you powerful backend primitives, but not a mobile-safe anonKey model like Supabase. Cloudflare KMP adds that missing application layer:
KMP app -> Cloudflare KMP SDK -> your Worker gateway -> D1 / KV / R2 / Durable Objects
Apps receive only a Worker URL and publishable key. Cloudflare account tokens, D1/KV/R2 bindings, and secrets stay server-side in your Worker.
This is an alpha scaffold. The SDK modules compile, the Kotlin/JS Worker dry-runs with Wrangler, and the Worker template implements D1 and KV routes.
Production gaps are explicit: R2 signing and Durable Object realtime are API-shaped but not fully implemented yet.
- Safe app-facing credentials — use
workerUrl+publishableKey, never Cloudflare account API tokens in the app - Type-safe Result monad —
CloudflareResult<T>withmap,flatMap,recover,onSuccess, andonFailure - D1 table API — Supabase-style
from("todos").select<Todo>(),insert,update, anddelete - KV helpers — read/write plain text or typed JSON through your Worker binding
- R2 API shape — signed upload/download URL client ready for Worker-side SigV4 implementation
- Realtime API surface — channel and broadcast model designed for Durable Object WebSockets
- Kotlin/JS Worker template — Kotlin-authored gateway compiled to a modern module Worker
- Modular SDK — install only the Cloudflare pieces your app needs
Add the dependencies you need to your build.gradle.kts.
// Version catalog (gradle/libs.versions.toml)
[versions]
cloudflare-kmp = "0.1.0-alpha01"
[libraries]
cloudflare-core = { module = "io.github.androidpoet:cloudflare-core", version.ref = "cloudflare-kmp" }
cloudflare-client = { module = "io.github.androidpoet:cloudflare-client", version.ref = "cloudflare-kmp" }
cloudflare-d1 = { module = "io.github.androidpoet:cloudflare-d1", version.ref = "cloudflare-kmp" }
cloudflare-kv = { module = "io.github.androidpoet:cloudflare-kv", version.ref = "cloudflare-kmp" }
cloudflare-r2 = { module = "io.github.androidpoet:cloudflare-r2", version.ref = "cloudflare-kmp" }
cloudflare-realtime = { module = "io.github.androidpoet:cloudflare-realtime", version.ref = "cloudflare-kmp" }// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.cloudflare.client) // includes cloudflare-core
implementation(libs.cloudflare.d1) // optional
implementation(libs.cloudflare.kv) // optional
implementation(libs.cloudflare.r2) // optional
implementation(libs.cloudflare.realtime) // optional
}
}
}Until the first Maven Central release is visible, use publishToMavenLocal or a GitHub Packages/maven repository from your fork.
val cloudflare = createCloudflareClient(
workerUrl = "https://api.example.workers.dev",
publishableKey = "cfpub_live_xxx",
)With user auth:
val cloudflare = createCloudflareClient(
workerUrl = "https://api.example.workers.dev",
publishableKey = "cfpub_live_xxx",
accessTokenProvider = { sessionStore.currentAccessToken },
)Every request sends:
x-cloudflare-publishable-key: cfpub_live_xxx
Authorization: Bearer optional-user-token@Serializable
data class Todo(
val id: String,
val title: String,
val done: Boolean,
)
val todos: CloudflareResult<List<Todo>> = cloudflare
.d1()
.from("todos")
.select<Todo> {
eq("done", "false")
order("created_at", descending = true)
limit(25)
}
todos.onSuccess { items ->
println("Got ${items.size} todos")
}.onFailure { error ->
println("Error: ${error.message}")
}Insert:
cloudflare
.d1()
.from("todos")
.insert(Todo(id = "todo_1", title = "Ship Cloudflare KMP", done = false))Update:
cloudflare
.d1()
.from("todos")
.update(Todo(id = "todo_1", title = "Ship Cloudflare KMP", done = true)) {
eq("id", "todo_1")
}Delete:
cloudflare
.d1()
.from("todos")
.delete {
eq("id", "todo_1")
}val kv = cloudflare.kv()
kv.putText("APP_KV", "settings/theme", "dark")
kv.get("APP_KV", "settings/theme").onSuccess { theme ->
println(theme)
}Typed JSON:
@Serializable
data class UserProfile(val name: String)
kv.putJson("APP_KV", "users/$userId/profile", UserProfile(name = "Ranbir"))
kv.getJson<UserProfile>("APP_KV", "users/$userId/profile")cloudflare
.r2()
.createUploadUrl(
bucket = "avatars",
path = "users/$userId/avatar.png",
contentType = "image/png",
)
.onSuccess { signedUrl ->
println("Upload with ${signedUrl.method}: ${signedUrl.url}")
}The SDK API is present. The MVP Worker returns 501 for R2 routes until Worker-side SigV4 signing is implemented.
val realtime = createRealtimeClient(
workerUrl = "https://api.example.workers.dev",
publishableKey = "cfpub_live_xxx",
)
realtime.connect()
val subscription = realtime.subscribe("room:lobby") { event ->
println("Event: $event")
}Realtime is currently an API surface. The planned transport is Worker WebSockets backed by Durable Objects.
The Worker template validates the publishable key and talks to Cloudflare bindings:
GET /health
GET /d1/{table}?eq.id=123&limit=1
POST /d1/{table}
PATCH /d1/{table}?eq.id=123
DELETE /d1/{table}?eq.id=123
GET /kv/{namespace}/{key}
POST /kv/{namespace}/{key}
DELETE /kv/{namespace}/{key}
Dry-run the Worker:
./gradlew :worker-template:jsProductionExecutableCompileSync
cd worker-template
wrangler deploy --dry-run --outdir distDeploy after replacing the D1/KV IDs in worker-template/wrangler.toml:
cd worker-template
npm install
npm run build
wrangler deploy| Module | Purpose |
|---|---|
cloudflare-core |
Shared config, headers, CloudflareResult, errors |
cloudflare-client |
Ktor HTTP transport and client factory |
cloudflare-d1 |
D1 table API |
cloudflare-kv |
KV text and JSON helpers |
cloudflare-r2 |
R2 signed URL API shape |
cloudflare-realtime |
Realtime channel API surface |
worker-template |
Kotlin/JS Worker gateway |
./gradlew :cloudflare-core:jvmTest \
:cloudflare-client:compileKotlinJvm \
:cloudflare-d1:compileKotlinJvm \
:cloudflare-kv:compileKotlinJvm \
:cloudflare-r2:compileKotlinJvm \
:cloudflare-realtime:compileKotlinJvm \
:worker-template:jsProductionExecutableCompileSyncMIT License. See LICENSE.
