Use SupaHost with Android and Kotlin
Connect an Android app to a SupaHost project with supabase-kt. Gradle dependencies, keys through BuildConfig, a client with Auth and Postgrest, a Compose list, and sign-in with an emailed code.
supabase-kt is the Kotlin client for Supabase, used on Android and in Kotlin Multiplatform. It's built on Ktor and kotlinx.serialization, and installs features as plugins. Give it your SupaHost project URL and anon key and it works unchanged.
The examples use Jetpack Compose and the recipes table from the quickstart.
Add the dependencies
In your app module's build.gradle.kts, add the serialization plugin, the supabase-kt BOM and the modules you need, plus a Ktor engine:
plugins {
// ...
kotlin("plugin.serialization") version "<your Kotlin version>"
}
dependencies {
implementation(platform("io.github.jan-tennert.supabase:bom:<supabase-kt version>"))
implementation("io.github.jan-tennert.supabase:postgrest-kt")
implementation("io.github.jan-tennert.supabase:auth-kt")
implementation("io.ktor:ktor-client-android:<ktor version>")
}
Take the current versions from the supabase-kt releases page on GitHub; each release lists the Kotlin and Ktor versions it needs. Add the internet permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Keep keys out of source
Add the Project URL and anon key from the Connect tab (subtab App) to local.properties, which Android Studio already ignores in git:
SUPABASE_URL=https://app-one.acme.supahost.space
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
Expose them through BuildConfig:
// app/build.gradle.kts
import java.util.Properties
val localProps = Properties().apply {
rootProject.file("local.properties").inputStream().use { load(it) }
}
android {
buildFeatures { buildConfig = true }
defaultConfig {
buildConfigField("String", "SUPABASE_URL", "\"${localProps["SUPABASE_URL"]}\"")
buildConfigField("String", "SUPABASE_ANON_KEY", "\"${localProps["SUPABASE_ANON_KEY"]}\"")
}
}
The anon key is fine inside an APK. The service_role key never is.
Create the client
// SupabaseClient.kt
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
val supabase = createSupabaseClient(
supabaseUrl = BuildConfig.SUPABASE_URL,
supabaseKey = BuildConfig.SUPABASE_ANON_KEY,
) {
install(Auth)
install(Postgrest)
}
Create it once and reuse it. The Auth plugin saves the session on the device and refreshes it.
Query a table
import io.github.jan.supabase.postgrest.from
import io.github.jan.supabase.postgrest.query.Columns
import io.github.jan.supabase.postgrest.query.Order
import kotlinx.serialization.Serializable
@Serializable
data class Recipe(val id: Long, val title: String, val minutes: Int? = null)
suspend fun loadRecipes(): List<Recipe> =
supabase.from("recipes")
.select(Columns.list("id", "title", "minutes")) {
order("created_at", Order.DESCENDING)
}
.decodeList<Recipe>()
Show them in Compose:
@Composable
fun RecipeList() {
var recipes by remember { mutableStateOf<List<Recipe>>(emptyList()) }
var error by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
runCatching { loadRecipes() }
.onSuccess { recipes = it }
.onFailure { error = it.message }
}
error?.let { Text("Error: $it") }
LazyColumn {
items(recipes, key = { it.id }) { r ->
Text("${r.title} (${r.minutes ?: "?"} min)", modifier = Modifier.padding(12.dp))
}
}
}
Inserting takes any serializable value:
@Serializable
data class NewRecipe(val title: String, val minutes: Int?)
supabase.from("recipes").insert(NewRecipe("Lemon rice", 30))
The insert only succeeds for a signed-in user, because the quickstart's insert policy is for authenticated.
Sign in
Email and password:
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.providers.builtin.Email
supabase.auth.signUpWith(Email) {
email = "[email protected]"
password = "a-long-password"
}
supabase.auth.signInWith(Email) {
email = "[email protected]"
password = "a-long-password"
}
supabase.auth.signOut()
An emailed one-time code is the easiest passwordless option on Android, because a typed code needs no redirect. (If you prefer links, add your app's scheme, such as recipebox://login, to Redirect URLs on the Auth tab and handle the deep link.)
import io.github.jan.supabase.auth.OtpType
import io.github.jan.supabase.auth.providers.builtin.OTP
supabase.auth.signInWith(OTP) { email = "[email protected]" }
// Later, with the code from the email:
supabase.auth.verifyEmailOtp(type = OtpType.Email.EMAIL, email = "[email protected]", token = code)
Watch the session
import io.github.jan.supabase.auth.status.SessionStatus
LaunchedEffect(Unit) {
supabase.auth.sessionStatus.collect { status ->
signedIn = status is SessionStatus.Authenticated
}
}
Google sign-in
Use Android's Credential Manager to get a Google ID token, then sign in with it. supabase-kt's compose-auth module can do both steps for you. With a token in hand:
import io.github.jan.supabase.auth.providers.Google
import io.github.jan.supabase.auth.providers.builtin.IDToken
supabase.auth.signInWith(IDToken) {
idToken = googleIdToken
provider = Google
}
Enable Google on the project's Auth tab first; see Sign in with Google. Browser-based OAuth needs an https:// App Link as the return address, listed under Redirect URLs.
Next steps
- Magic links and email OTP
- Uploading files (add the
storage-ktmodule) - Realtime on SupaHost (add
realtime-kt) - Can I use SupaHost for a mobile app?