Skip to content

Instantly share code, notes, and snippets.

View AndroidPoet's full-sized avatar
God Mode

Android Poet AndroidPoet

God Mode
View GitHub Profile

I Built a Leaner Supabase SDK for Kotlin Multiplatform — Here’s Why It’s Architecturally Better

How trading 19,000 lines of code for 4,000 improved Type Safety, explicit error handling, and dependency injection.

When I first started building Kotlin Multiplatform apps with Supabase, I reached for the official SDK. It worked — it had every feature I needed: Auth, PostgREST, Storage, Realtime, Edge Functions. But every time I used it, something felt off.

The exceptions. The string IDs. The framework-heavy dependency model. The documentation felt like a minefield; every method signature was a gamble of six possible exceptions.

So I asked myself: What would a Supabase SDK look like if we built it the way Kotlin was meant to be used?

supabase.auth.signInWithEmail(
email = "user@example.com",
password = "password"
).onSuccess { session ->
sessionManager.saveSession(session)
}.onFailure { error ->
println("Login failed: ${error.message}")
}
try {
supabase.auth.signInWith(Email) {
email = "user@example.com"
password = "password"
}
} catch (e: Exception) {
// Handle... whatever happened
}
database.selectTyped<Product>("products") {
eq("category", "electronics")
gt("price", "100")
order("created_at", ascending = false)
limit(25)
}.map { response ->
JSON.decodeFromString<List<Product>>(response)
}.onSuccess { products ->
println("Found ${products.size} products")
}.onFailure { error ->
val result = supabase.postgrest["products"].select {
Product::id eq 2
Product::category eq "electronics"
Product::price gt 100
order(Product::createdAt, Order.DESCENDING)
limit(25)
}.decodeList<Product>()
// First-class Koin support — no manual wiring
startKoin {
modules(
supabaseModule(
projectUrl = "https://myproj.supabase.co",
apiKey = "anon-key"
),
authModule(),
databaseModule,
storageModule,
kotlin {
sourceSets {
commonMain.dependencies {
implementation(supabase.client) // core + HTTP
implementation(supabase.auth) // optional
implementation(supabase.database) // optional
implementation(supabase.storage) // optional
// Pick only what you use
}
}
val userId = UserId("user_123")
val postId = PostId("post_456")
database.delete("posts").eq("id", userId) // COMPILE ERROR!
@JvmInline
@Serializable
public value class UserId(public val value: String)
@JvmInline
@Serializable
public value class SessionId(public val value: String)
@JvmInline
@Serializable
public sealed interface SupabaseResult<out T> {
public data class Success<out T>(public val value: T) : SupabaseResult<T>
public data class Failure(public val error: SupabaseError) : SupabaseResult<Nothing>
public val isSuccess: Boolean get() = this is Success
public val isFailure: Boolean get() = this is Failure
}
public inline fun <T, R> SupabaseResult<T>.map(transform: (T) -> R): SupabaseResult<R>
public inline fun <T, R> SupabaseResult<T>.flatMap(transform: (T) -> SupabaseResult<R>): SupabaseResult<R>