
Type-safe, coroutine-first, modular client for Supabase with Result monad, value-class IDs, PostgREST filter DSL, OAuth+MFA, session auto-refresh, realtime, storage, and edge functions.
Kotlin Multiplatform SDK for Supabase — type-safe, coroutine-first, modular client for every platform Kotlin runs on.
SupabaseResult<T> with map, flatMap, recover — no exceptions leak to callersUserId, BucketId, SessionId, ChannelId prevent mixups at compile timeeq, neq, gt, like, ilike, in, is, textSearch, contains, and moreSessionState observation via StateFlow
Add the dependencies you need to your build.gradle.kts:
[versions]
supabase-kmp = "0.3.2"
[libraries]
supabase-core = { module = "io.github.androidpoet:supabase-core", version.ref = "supabase-kmp" }
supabase-client = { module = "io.github.androidpoet:supabase-client", version.ref = "supabase-kmp" }
supabase-auth = { module = "io.github.androidpoet:supabase-auth", version.ref = "supabase-kmp" }
supabase-auth-admin = { module = "io.github.androidpoet:supabase-auth-admin", version.ref = "supabase-kmp" }
supabase-database = { module = "io.github.androidpoet:supabase-database", version.ref = "supabase-kmp" }
supabase-storage = { module = "io.github.androidpoet:supabase-storage", version.ref = "supabase-kmp" }
supabase-realtime = { module = "io.github.androidpoet:supabase-realtime", version.ref = "supabase-kmp" }
supabase-functions = { module = "io.github.androidpoet:supabase-functions", version.ref = "supabase-kmp" }kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.supabase.client)
implementation(libs.supabase.auth)
// Service-role/admin APIs only. Do not use in anon-key mobile clients.
implementation(libs.supabase.auth.admin)
implementation(libs.supabase.database)
implementation(libs.supabase.storage)
implementation(libs.supabase.realtime)
implementation(libs.supabase.functions)
}
}
}val client = Supabase.create(
projectUrl = "https://your-project.supabase.co",
apiKey = "your-anon-key",
) {
logging = true
}
val auth = createAuthClient(client)
val database = createDatabaseClient(client)
val storage = createStorageClient(client)
val realtime = createRealtimeClient(client)
val functions = createFunctionsClient(client)@Serializable
data class Todo(val id: String, val title: String, val done: Boolean)
@Serializable
data class TodoPatch(val done: Boolean)
@Serializable
data class DashboardStatsRequest(val user_id: String)
@Serializable
data class DashboardStats(val total: Int)
val todos: SupabaseResult<List<Todo>> = database.selectTyped<Todo>(
table = "todos",
) {
eq("done", "false")
gt("priority", "3")
order("created_at", ascending = false)
limit(25)
}
todos.onSuccess { items ->
println("Got ${items.size} todos")
}.onFailure { error ->
println("Error: ${error.message}")
}
val result = database.insertTyped(
table = "todos",
value = Todo(id = "1", title = "Ship it", done = false),
)
database.updateTyped(
table = "todos",
value = TodoPatch(done = true),
) {
eq("id", "1")
}
val stats: SupabaseResult<DashboardStats> = database.rpcTyped<DashboardStatsRequest, DashboardStats>(
function = "get_dashboard_stats",
request = DashboardStatsRequest(user_id = "123"),
)val auth = createAuthClient(client)
val sessionManager = createSessionManager(authClient = auth, supabaseClient = client)
auth.signUpWithEmail(
email = "user@example.com",
password = "secure-password",
).onSuccess { session ->
sessionManager.saveSession(session)
}
auth.signInWithEmail(
email = "user@example.com",
password = "secure-password",
).onSuccess { session ->
sessionManager.saveSession(session)
}
val oauthUrl = auth.getOAuthSignInUrl(
provider = OAuthProvider.GOOGLE,
redirectTo = "myapp://callback",
)
val pkce = auth.generatePkceParams()
auth.exchangeCodeForSession(authCode = "code-from-callback", codeVerifier = pkce.codeVerifier)
val accessToken = sessionManager.accessToken!!
auth.mfaEnroll(factorType = MfaFactorType.TOTP, accessToken = accessToken).onSuccess { factor ->
auth.mfaVerify(
factorId = factor.id,
challengeId = "challenge-id",
code = "123456",
accessToken = accessToken,
)
}
sessionManager.sessionState.collect { state ->
when (state) {
is SessionState.Authenticated -> println("User: ${state.session.user.id}")
is SessionState.Expired -> println("Session expired, refreshing...")
SessionState.NotAuthenticated -> println("Signed out")
SessionState.Loading -> println("Loading...")
}
}val storage = createStorageClient(client)
storage.upload(
bucket = "avatars",
path = "user123/avatar.png",
data = imageBytes,
contentType = "image/png",
).onSuccess { key ->
println("Uploaded: $key")
}
storage.createSignedUrl(
bucket = "avatars",
path = "user123/avatar.png",
expiresIn = 3600,
).onSuccess { url ->
println("Signed URL: $url")
}
val publicUrl = storage.getPublicUrl(bucket = "avatars", path = "user123/avatar.png")
storage.list(bucket = "avatars", prefix = "user123/").onSuccess { files ->
files.forEach { println(it.name) }
}val realtime = createRealtimeClient(client)
realtime.connect()
realtime.connectionState.collect { state ->
when (state) {
is ConnectionState.Connected -> println("Connected")
is ConnectionState.Reconnecting -> println("Reconnecting attempt ${state.attempt}...")
is ConnectionState.Failed -> println("Failed: ${state.reason}")
else -> {}
}
}
val subscription = realtime.channel("todos")
.onPostgresChange(table = "todos", event = PostgresChangeEvent.INSERT) { record ->
println("New todo: $record")
}
.onPostgresChange(table = "todos", event = PostgresChangeEvent.DELETE) { record ->
println("Deleted: $record")
}
.subscribe()
realtime.channel("room:lobby")
.onPresence { state ->
println("Online users: ${state.size}")
}
.subscribe()
subscription.broadcast(event = "cursor", payload = buildJsonObject {
put("x", 100)
put("y", 200)
})
subscription.unsubscribe()
realtime.disconnect()val functions = createFunctionsClient(client)
functions.invoke(
functionName = "hello-world",
body = """{"name": "Kotlin"}""",
).onSuccess { data ->
println("Response: $data")
}
functions.invokeTyped<WelcomeResponse>(
functionName = "hello-world",
body = """{"name": "Kotlin"}""",
).onSuccess { response ->
println("Message: ${response.message}")
}
functions.invokeWithBody(
functionName = "process-image",
body = imageBytes,
contentType = "image/png",
)┌─────────────────────────────────────────────────────────────────────┐
│ Your App │
├──────────┬───────────┬───────────┬───────────┬───────────┬─────────┤
│ supabase │ supabase │ supabase │ supabase │ supabase │supabase │
│ auth │ database │ storage │ realtime │ functions │ client │
│ │ │ │ │ │ │
│ OAuth │ PostgREST │ Buckets │ WebSocket │ Invoke │ HTTP │
│ MFA/PKCE │ Filter DSL│ Upload │ Phoenix │ Typed Req │ Auth │
│ Session │ RPC │ SignedURL │ Presence │ Response │ Factory │
│ Manager │ Typed Ext │ Public URL│ Reconnect │ Binary │ Config │
├──────────┴───────────┴───────────┼───────────┤ │ │
│ │ Ktor │ │ │
│ │ Engines │ │ │
├───────────────────────────────────┴───────────┤ │ │
│ supabase-core │ │ │
│ SupabaseResult · Value IDs · Filter DSL │ │ │
│ Error Types · Response Models │ │ │
└────────────────────────────────────────────────┴───────────┴─────────┘
| Module | Artifact | Description |
|---|---|---|
| supabase-core | io.github.androidpoet:supabase-core |
Result monad, error types, value class IDs, filter DSL |
| supabase-client | io.github.androidpoet:supabase-client |
HTTP transport, platform engines, auth state, factory wiring |
| supabase-auth | io.github.androidpoet:supabase-auth |
Email, phone, OTP, OAuth (17 providers), MFA, PKCE, session management |
| supabase-database | io.github.androidpoet:supabase-database |
PostgREST CRUD, RPC, typed filter extensions |
| supabase-storage | io.github.androidpoet:supabase-storage |
Bucket CRUD, file upload/download, signed & public URLs |
| supabase-realtime | io.github.androidpoet:supabase-realtime |
WebSocket (Phoenix protocol), auto-reconnect, broadcast, presence |
| supabase-functions | io.github.androidpoet:supabase-functions |
Edge function invocation with typed responses |
| Platform | Target | Ktor Engine |
|---|---|---|
| Android | androidTarget() |
OkHttp |
| JVM | jvm() |
OkHttp |
| iOS |
iosX64() iosArm64() iosSimulatorArm64()
|
Darwin |
| macOS |
macosX64() macosArm64()
|
Darwin |
| tvOS |
tvosX64() tvosArm64() tvosSimulatorArm64()
|
Darwin |
| watchOS |
watchosX64() watchosArm64() watchosSimulatorArm64()
|
Darwin |
| Linux | linuxX64() |
CIO |
| Windows | mingwX64() |
CIO |
| Web | wasmJs() |
Js |
| supabase-kmp | supabase-kt (official) | |
|---|---|---|
| Codebase | ~3K LOC | ~26K LOC |
| Error handling |
SupabaseResult<T> monad |
Thrown exceptions |
| Type safety | Value class IDs | String IDs |
| Dependencies | 3 core | 7+ |
| Session mgmt |
SessionManager + StateFlow
|
Built-in (heavier) |
| Reconnection | Exponential backoff | Exponential backoff |
| Targets | 15 | 15+ |
| Layer | Library |
|---|---|
| Language | Kotlin 2.1.10 |
| Networking | Ktor 3.1.1 |
| Serialization | kotlinx.serialization 1.8.0 |
| Coroutines | kotlinx.coroutines 1.10.1 |
| Publishing | vanniktech maven-publish 0.30.0 |
# Compile all targets
./gradlew compileKotlinJvm
# Run tests
./gradlew jvmTest
# Full build (all platforms)
./gradlew build --no-configuration-cache
# Publish to Maven Central (CI only, auto-release)
./gradlew publishAndReleaseToMavenCentral --no-configuration-cacheCHANGELOG.md
docs/
.gitbook.yaml
docs/guides/gitbook-publishing.md
mkdocs.yml
requirements-docs.txt
python -m pip install -r requirements-docs.txt
mkdocs servemkdocs gh-deploy --forceMIT License
Copyright (c) 2026 Ranbir Singh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Kotlin Multiplatform SDK for Supabase — type-safe, coroutine-first, modular client for every platform Kotlin runs on.
SupabaseResult<T> with map, flatMap, recover — no exceptions leak to callersUserId, BucketId, SessionId, ChannelId prevent mixups at compile timeeq, neq, gt, like, ilike, in, is, textSearch, contains, and moreSessionState observation via StateFlow
Add the dependencies you need to your build.gradle.kts:
[versions]
supabase-kmp = "0.3.2"
[libraries]
supabase-core = { module = "io.github.androidpoet:supabase-core", version.ref = "supabase-kmp" }
supabase-client = { module = "io.github.androidpoet:supabase-client", version.ref = "supabase-kmp" }
supabase-auth = { module = "io.github.androidpoet:supabase-auth", version.ref = "supabase-kmp" }
supabase-auth-admin = { module = "io.github.androidpoet:supabase-auth-admin", version.ref = "supabase-kmp" }
supabase-database = { module = "io.github.androidpoet:supabase-database", version.ref = "supabase-kmp" }
supabase-storage = { module = "io.github.androidpoet:supabase-storage", version.ref = "supabase-kmp" }
supabase-realtime = { module = "io.github.androidpoet:supabase-realtime", version.ref = "supabase-kmp" }
supabase-functions = { module = "io.github.androidpoet:supabase-functions", version.ref = "supabase-kmp" }kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.supabase.client)
implementation(libs.supabase.auth)
// Service-role/admin APIs only. Do not use in anon-key mobile clients.
implementation(libs.supabase.auth.admin)
implementation(libs.supabase.database)
implementation(libs.supabase.storage)
implementation(libs.supabase.realtime)
implementation(libs.supabase.functions)
}
}
}val client = Supabase.create(
projectUrl = "https://your-project.supabase.co",
apiKey = "your-anon-key",
) {
logging = true
}
val auth = createAuthClient(client)
val database = createDatabaseClient(client)
val storage = createStorageClient(client)
val realtime = createRealtimeClient(client)
val functions = createFunctionsClient(client)@Serializable
data class Todo(val id: String, val title: String, val done: Boolean)
@Serializable
data class TodoPatch(val done: Boolean)
@Serializable
data class DashboardStatsRequest(val user_id: String)
@Serializable
data class DashboardStats(val total: Int)
val todos: SupabaseResult<List<Todo>> = database.selectTyped<Todo>(
table = "todos",
) {
eq("done", "false")
gt("priority", "3")
order("created_at", ascending = false)
limit(25)
}
todos.onSuccess { items ->
println("Got ${items.size} todos")
}.onFailure { error ->
println("Error: ${error.message}")
}
val result = database.insertTyped(
table = "todos",
value = Todo(id = "1", title = "Ship it", done = false),
)
database.updateTyped(
table = "todos",
value = TodoPatch(done = true),
) {
eq("id", "1")
}
val stats: SupabaseResult<DashboardStats> = database.rpcTyped<DashboardStatsRequest, DashboardStats>(
function = "get_dashboard_stats",
request = DashboardStatsRequest(user_id = "123"),
)val auth = createAuthClient(client)
val sessionManager = createSessionManager(authClient = auth, supabaseClient = client)
auth.signUpWithEmail(
email = "user@example.com",
password = "secure-password",
).onSuccess { session ->
sessionManager.saveSession(session)
}
auth.signInWithEmail(
email = "user@example.com",
password = "secure-password",
).onSuccess { session ->
sessionManager.saveSession(session)
}
val oauthUrl = auth.getOAuthSignInUrl(
provider = OAuthProvider.GOOGLE,
redirectTo = "myapp://callback",
)
val pkce = auth.generatePkceParams()
auth.exchangeCodeForSession(authCode = "code-from-callback", codeVerifier = pkce.codeVerifier)
val accessToken = sessionManager.accessToken!!
auth.mfaEnroll(factorType = MfaFactorType.TOTP, accessToken = accessToken).onSuccess { factor ->
auth.mfaVerify(
factorId = factor.id,
challengeId = "challenge-id",
code = "123456",
accessToken = accessToken,
)
}
sessionManager.sessionState.collect { state ->
when (state) {
is SessionState.Authenticated -> println("User: ${state.session.user.id}")
is SessionState.Expired -> println("Session expired, refreshing...")
SessionState.NotAuthenticated -> println("Signed out")
SessionState.Loading -> println("Loading...")
}
}val storage = createStorageClient(client)
storage.upload(
bucket = "avatars",
path = "user123/avatar.png",
data = imageBytes,
contentType = "image/png",
).onSuccess { key ->
println("Uploaded: $key")
}
storage.createSignedUrl(
bucket = "avatars",
path = "user123/avatar.png",
expiresIn = 3600,
).onSuccess { url ->
println("Signed URL: $url")
}
val publicUrl = storage.getPublicUrl(bucket = "avatars", path = "user123/avatar.png")
storage.list(bucket = "avatars", prefix = "user123/").onSuccess { files ->
files.forEach { println(it.name) }
}val realtime = createRealtimeClient(client)
realtime.connect()
realtime.connectionState.collect { state ->
when (state) {
is ConnectionState.Connected -> println("Connected")
is ConnectionState.Reconnecting -> println("Reconnecting attempt ${state.attempt}...")
is ConnectionState.Failed -> println("Failed: ${state.reason}")
else -> {}
}
}
val subscription = realtime.channel("todos")
.onPostgresChange(table = "todos", event = PostgresChangeEvent.INSERT) { record ->
println("New todo: $record")
}
.onPostgresChange(table = "todos", event = PostgresChangeEvent.DELETE) { record ->
println("Deleted: $record")
}
.subscribe()
realtime.channel("room:lobby")
.onPresence { state ->
println("Online users: ${state.size}")
}
.subscribe()
subscription.broadcast(event = "cursor", payload = buildJsonObject {
put("x", 100)
put("y", 200)
})
subscription.unsubscribe()
realtime.disconnect()val functions = createFunctionsClient(client)
functions.invoke(
functionName = "hello-world",
body = """{"name": "Kotlin"}""",
).onSuccess { data ->
println("Response: $data")
}
functions.invokeTyped<WelcomeResponse>(
functionName = "hello-world",
body = """{"name": "Kotlin"}""",
).onSuccess { response ->
println("Message: ${response.message}")
}
functions.invokeWithBody(
functionName = "process-image",
body = imageBytes,
contentType = "image/png",
)┌─────────────────────────────────────────────────────────────────────┐
│ Your App │
├──────────┬───────────┬───────────┬───────────┬───────────┬─────────┤
│ supabase │ supabase │ supabase │ supabase │ supabase │supabase │
│ auth │ database │ storage │ realtime │ functions │ client │
│ │ │ │ │ │ │
│ OAuth │ PostgREST │ Buckets │ WebSocket │ Invoke │ HTTP │
│ MFA/PKCE │ Filter DSL│ Upload │ Phoenix │ Typed Req │ Auth │
│ Session │ RPC │ SignedURL │ Presence │ Response │ Factory │
│ Manager │ Typed Ext │ Public URL│ Reconnect │ Binary │ Config │
├──────────┴───────────┴───────────┼───────────┤ │ │
│ │ Ktor │ │ │
│ │ Engines │ │ │
├───────────────────────────────────┴───────────┤ │ │
│ supabase-core │ │ │
│ SupabaseResult · Value IDs · Filter DSL │ │ │
│ Error Types · Response Models │ │ │
└────────────────────────────────────────────────┴───────────┴─────────┘
| Module | Artifact | Description |
|---|---|---|
| supabase-core | io.github.androidpoet:supabase-core |
Result monad, error types, value class IDs, filter DSL |
| supabase-client | io.github.androidpoet:supabase-client |
HTTP transport, platform engines, auth state, factory wiring |
| supabase-auth | io.github.androidpoet:supabase-auth |
Email, phone, OTP, OAuth (17 providers), MFA, PKCE, session management |
| supabase-database | io.github.androidpoet:supabase-database |
PostgREST CRUD, RPC, typed filter extensions |
| supabase-storage | io.github.androidpoet:supabase-storage |
Bucket CRUD, file upload/download, signed & public URLs |
| supabase-realtime | io.github.androidpoet:supabase-realtime |
WebSocket (Phoenix protocol), auto-reconnect, broadcast, presence |
| supabase-functions | io.github.androidpoet:supabase-functions |
Edge function invocation with typed responses |
| Platform | Target | Ktor Engine |
|---|---|---|
| Android | androidTarget() |
OkHttp |
| JVM | jvm() |
OkHttp |
| iOS |
iosX64() iosArm64() iosSimulatorArm64()
|
Darwin |
| macOS |
macosX64() macosArm64()
|
Darwin |
| tvOS |
tvosX64() tvosArm64() tvosSimulatorArm64()
|
Darwin |
| watchOS |
watchosX64() watchosArm64() watchosSimulatorArm64()
|
Darwin |
| Linux | linuxX64() |
CIO |
| Windows | mingwX64() |
CIO |
| Web | wasmJs() |
Js |
| supabase-kmp | supabase-kt (official) | |
|---|---|---|
| Codebase | ~3K LOC | ~26K LOC |
| Error handling |
SupabaseResult<T> monad |
Thrown exceptions |
| Type safety | Value class IDs | String IDs |
| Dependencies | 3 core | 7+ |
| Session mgmt |
SessionManager + StateFlow
|
Built-in (heavier) |
| Reconnection | Exponential backoff | Exponential backoff |
| Targets | 15 | 15+ |
| Layer | Library |
|---|---|
| Language | Kotlin 2.1.10 |
| Networking | Ktor 3.1.1 |
| Serialization | kotlinx.serialization 1.8.0 |
| Coroutines | kotlinx.coroutines 1.10.1 |
| Publishing | vanniktech maven-publish 0.30.0 |
# Compile all targets
./gradlew compileKotlinJvm
# Run tests
./gradlew jvmTest
# Full build (all platforms)
./gradlew build --no-configuration-cache
# Publish to Maven Central (CI only, auto-release)
./gradlew publishAndReleaseToMavenCentral --no-configuration-cacheCHANGELOG.md
docs/
.gitbook.yaml
docs/guides/gitbook-publishing.md
mkdocs.yml
requirements-docs.txt
python -m pip install -r requirements-docs.txt
mkdocs servemkdocs gh-deploy --forceMIT License
Copyright (c) 2026 Ranbir Singh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.