Best Tutorial for Android Jetpack Compose

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

Supabase Authentication in Jetpack Compose

Supabase Authentication in Jetpack Compose - Coding Bihar
Supabase Authentication in Jetpack Compose

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 Organization Supabase Authentication
Create a New Organization Supabase Authentication
  • Create a new project in the dashboard.
Create a new project in the dashboard.

Create a new project in the Supabase 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).
Supabase Project public API Key

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:
RegisterScreen
LoginScreen
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")
        }
    }
}

OUTPUT:


Supabase Authentication in Jetpack Compose Screenshot 5

Supabase Authentication in Jetpack Compose Screenshot 1

Supabase Authentication in Jetpack Compose Screenshot 2

Supabase Authentication in Jetpack Compose Screenshot 3

Supabase Authentication in Jetpack Compose Screenshot 4
My Supabase Authentication App using Jetpack Compose

Special Message

Welcome to Coding