Using Supabase Authentication in Jetpack Compose: A Complete Guide
When building modern Android apps, authentication is almost always required. Whether it’s a shopping app, a chat app, or even a personal project, you’ll need a secure way to let users sign up, sign in, and manage their accounts. Traditionally, this meant setting up your own backend with Firebase or a custom server. But in recent years, Supabase has emerged as one of the best alternatives for developers who want an open-source, PostgreSQL-powered backend that works beautifully with Android.
In this article, we’ll walk through how to integrate Supabase Authentication in a Jetpack Compose Android app. We’ll cover the following:
- What Supabase is and why it’s great for Android apps
- Setting up your Supabase project
- Adding the Supabase Kotlin SDK to your app
- Implementing authentication (sign up, sign in, sign out)
- Building a simple Jetpack Compose UI for auth flows
- Handling errors and best practices
By the end, you’ll have a clear understanding of how to wire up Supabase Auth with your Compose app, along with reusable code you can adapt to your projects.
🔹 What is Supabase?
If Firebase had an open-source twin, it would be Supabase — built with the same spirit but with transparency and community control at its core. It provides:
- A PostgreSQL database with real-time subscriptions
- Authentication and authorization (email/password, OAuth, magic links, etc.)
- Storage for files and media
- Edge functions to run server-side code
- Supabase stands out from Firebase by being open-source and running on PostgreSQL, giving developers the flexibility to avoid vendor lock-in. For Android developers, this means flexibility, transparency, and cost efficiency.
🔹 Why Use Supabase for Authentication?
Supabase Auth is built on GoTrue, a service that provides secure user management. With it, you can:
- Let people join your app by signing up with just an email and a password.
- Enable OAuth providers like Google, GitHub, Twitter, etc.
- Manage sessions securely with JWT tokens
- Handle password resets and email verification
This makes it perfect for Android apps where authentication is a must-have.
🔹 Setting Up Supabase
Before diving into code, you need a Supabase project:
- Go to Supabase and create an account.
- Create a New Organization
- Create a new project in the dashboard.
- Once it’s ready, go to Project Settings → API.
- Copy the Project URL (https://xyzcompany.supabase.co)
- Copy the anon public API key (used in your Android app).
We’ll use these in the client initialization step.
🔹 Adding Supabase to Your Android Project
1. Add Dependencies
Add the Supabase Kotlin dependencies (auth-kt, ktor-client-okhttp, navigation compose and bom for version control).
implementation(platform("io.github.jan-tennert.supabase:bom:3.1.1"))
implementation("io.github.jan-tennert.supabase:auth-kt")
implementation("io.ktor:ktor-client-okhttp:3.0.3")
implementation("androidx.navigation:navigation-compose:2.9.3")
Sync Gradle.2. Initialize Supabase with Auth Manager
Make a small class AuthManager to initialize Supabase client with:
- Project URL
- Anon Key (API key from Supabase dashboard)
- This instance will be used everywhere in your app.
- and handle login, signup, logout.
This instance will be used everywhere in your app.
It will call Supabase Auth methods and return results (success or error).
This keeps your UI screens clean.
AuthManager
package com.codingbihar.mysupabase
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.providers.builtin.Email
import io.github.jan.supabase.createSupabaseClient
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class AuthManager() {
private val supabase = createSupabaseClient(
supabaseUrl = "add your project url",
supabaseKey = "add your api key"
) {
install(Auth)
}
fun signUpWithEmail(emailValue: String, passwordValue: String): Flow = flow {
try {
supabase.auth.signUpWith(Email) {
email = emailValue
password = passwordValue
}
emit(AuthResponse.Success)
} catch (e: Exception) {
emit(AuthResponse.Error(e.localizedMessage))
}
}
fun signInWithEmail(emailValue: String, passwordValue: String): Flow = flow {
try {
supabase.auth.signInWith(Email) {
email = emailValue
password = passwordValue
}
emit(AuthResponse.Success)
} catch (e: Exception) {
emit(AuthResponse.Error(e.localizedMessage))
}
}
}
3. AuthResponse
Made a sealed interface which is the right way to represent success or failure in authentication.
It has:
- Success → when login/register works
- Error(message: String?) → when something goes wrong
AuthResponse
package com.codingbihar.mysupabase
sealed interface AuthResponse {
data object Success : AuthResponse
data class Error(val message: String?) : AuthResponse
}
4. Navigation Setup
Use Navigation Compose.
Define routes/screens:
Define routes/screens:
RegisterScreen
LoginScreen
HomeScreen
HomeScreen
APPNav.kt
package com.codingbihar.mysupabase
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@Composable
fun AppNav() {
val nav = rememberNavController()
NavHost(navController = nav, startDestination = "signup" ) {
composable ("signup"){
RegisterScreen(navController = nav)
}
composable ("home"){
HomeScreen(navController = nav)
}
composable ("login"){
LoginScreen(navController = nav)
}
}
}
UI Screens (Register, Login and Home)
Register
package com.codingbihar.mysupabase
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@Composable
fun RegisterScreen(navController: NavController) {
var isSigningUp by remember { mutableStateOf(false) }
var emailValue by remember {
mutableStateOf("")
}
var passwordValue by remember {
mutableStateOf("")
}
LocalContext.current
val authManager = remember {
AuthManager()
}
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black),
contentAlignment = Alignment.TopCenter
) {
Gradient()
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
.padding(top = 110.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
RegisterHeader()
Spacer(modifier = Modifier.height(40.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 30.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color.White.copy(alpha = 0.2f))
)
Text(
text = "Or",
color = Color.White.copy(alpha = 0.7f),
modifier = Modifier.padding(horizontal = 10.dp)
)
Box(
modifier = Modifier
.weight(1f)
.height(1.dp)
.background(Color.White.copy(alpha = 0.2f))
)
}
Column(
horizontalAlignment = Alignment.Start
) {
Text(
text = "Email",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
TextField(
value = emailValue,
onValueChange = { newValue ->
emailValue = newValue
},
placeholder = {
Text(
text = "sandeep@codingbihar.com",
color = Color.White.copy(alpha = 0.7f)
)
},
shape = RoundedCornerShape(10.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.DarkGray,
unfocusedContainerColor = Color.DarkGray
),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(20.dp))
Column(
horizontalAlignment = Alignment.Start
) {
Text(
text = "Password",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
TextField(
value = passwordValue,
onValueChange = { newValue ->
passwordValue = newValue
},
placeholder = {
Text(
text = "Enter your password",
color = Color.White.copy(alpha = 0.7f)
)
},
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(10.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.DarkGray,
unfocusedContainerColor = Color.DarkGray
),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(35.dp))
Button(
onClick = {
isSigningUp = true // Start showing progress
authManager.signUpWithEmail(emailValue, passwordValue)
.onEach { result ->
when (result) {
is AuthResponse.Success -> {
Log.d("auth", "Email Success")
navController.navigate("home") {
popUpTo("login") { inclusive = true }
}
}
is AuthResponse.Error -> {
Log.e("auth", "Email Failed: ${result.message}")
}
}
isSigningUp = false // Stop showing progress
}
.launchIn(coroutineScope)
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.White
),
shape = RoundedCornerShape(10.dp),
modifier = Modifier.fillMaxWidth(),
enabled = !isSigningUp // Disable button while loading
) {
if (isSigningUp) {
CircularProgressIndicator(
color = Color.Black,
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text("Signing Up...", color = Color.Black)
} else {
Text(
text = "Sign Up",
color = Color.Black,
modifier = Modifier.padding(vertical = 4.dp)
)
}
}
Spacer(modifier = Modifier.height(25.dp))
TextButton(
onClick = {
navController.navigate("login")
}
) {
Text(
text = buildAnnotatedString {
withStyle(
style = SpanStyle(
fontWeight = FontWeight.Light,
color = Color.White.copy(alpha = 0.8f)
)
) {
append("Already have an account? ")
}
withStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
color = Color.White
)
) {
append("Log in")
}
}
)
}
}
}
}
@Composable
private fun RegisterHeader() {
Text(
text = "Create An Account",
style = MaterialTheme.typography.titleLarge,
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter your personal data to create an account",
style = MaterialTheme.typography.bodyMedium,
color = Color.White
)
}
@Composable
fun Gradient() {
val purple = Color(0xFF9C27B0)
val darkPurple = Color(0xFF6A1B9A)
val black = Color.Black
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.35f)
.background(
brush = Brush.verticalGradient(
colors = listOf(
purple,
darkPurple,
black
)
)
)
)
}
Login
package com.codingbihar.mysupabase
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@Composable
fun LoginScreen(navController: NavController) {
var emailValue by remember { mutableStateOf("") }
var passwordValue by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf(null) }
LocalContext.current
val authManager = remember {
AuthManager()
}
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(Color(0xFF6A1B9A), Color(0xFF9C27B0), Color.Black)
)
)
.padding(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
) {
Text(
text = "Welcome Back",
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.ExtraBold
)
Spacer(modifier = Modifier.height(40.dp))
// Email
Text(
text = "Email",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
TextField(
value = emailValue,
onValueChange = { emailValue = it },
placeholder = { Text("sandeep@codingbihar.com", color = Color.White.copy(alpha = 0.7f)) },
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.DarkGray,
unfocusedContainerColor = Color.DarkGray
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
// Password
Text(
text = "Password",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
TextField(
value = passwordValue,
onValueChange = { passwordValue = it },
placeholder = { Text("Enter your password", color = Color.White.copy(alpha = 0.7f)) },
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp),
colors = TextFieldDefaults.colors(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.DarkGray,
unfocusedContainerColor = Color.DarkGray
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(30.dp))
// Error Message
errorMessage?.let {
Text(it, color = Color.Red, modifier = Modifier.padding(bottom = 10.dp))
}
// Login Button
Button(
onClick = {
isLoading = true
authManager.signInWithEmail(emailValue, passwordValue)
.onEach { result ->
isLoading = false
when (result) {
is AuthResponse.Success -> {
navController.navigate("home") {
popUpTo("login") { inclusive = true }
}
}
is AuthResponse.Error -> {
errorMessage = result.message
}
}
}
.launchIn(coroutineScope)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(8.dp))
Text("Logging In...")
} else {
Text("Login")
}
}
}
}
}
Home
package com.codingbihar.mysupabase
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
@Composable
fun HomeScreen(navController: NavHostController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Text("Welcome to Coding Bihar Supabase Auth Home Screen!",
style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
navController.navigate("signup") {
popUpTo("home") { inclusive = true } } }) {
Text("Logout")
}
}
}