Docs

Quickstart: Android (Jetpack Compose)

Build the notes app with Kotlin and Jetpack Compose: keep the session in encrypted storage, follow sign-in state with auth.changes, and list and add the signed-in user's notes.

Updated Oct 24, 2026

A Compose app (Android 8, API 26+) where people sign up, sign in, add notes and see only their own.

#Before you start

Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.

#1. Create the app and add the dependency

In Android Studio create an Empty Activity project (Kotlin, Compose). Add the SDK and AndroidX Security for encrypted storage, and the internet permission.

dependencies {
    implementation("in.sovaria:sovaria:1.0.0")
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
}

#2. Keep the session in encrypted storage

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import `in`.sovaria.SessionStorage

class KeystoreStore(context: Context) : SessionStorage {
    private val prefs = EncryptedSharedPreferences.create(
        context, "sovaria",
        MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
    )
    override suspend fun read(key: String): String? = prefs.getString(key, null)
    override suspend fun write(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    override suspend fun delete(key: String) { prefs.edit().remove(key).apply() }
}

#3. Connect and follow sign-in state

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import `in`.sovaria.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

class NotesViewModel(app: Application) : AndroidViewModel(app) {
    val signedIn = MutableStateFlow(false)
    val notes = MutableStateFlow<List<Map<String, Any?>>>(emptyList())
    val error = MutableStateFlow("")
    private lateinit var sovaria: Sovaria

    init {
        viewModelScope.launch {
            // Restores the saved session (refreshing it if it's about to expire).
            sovaria = Sovaria.initialize(
                url = "https://<project-ref>.sovarias.in",
                anonKey = "<anon-key>",
                storage = KeystoreStore(app),
            )
            sovaria.auth.changes.collect { state ->      // first event: INITIAL_SESSION
                signedIn.value = state.session != null
                if (signedIn.value) load()
            }
        }
    }

    private fun run(block: suspend () -> Unit) = viewModelScope.launch {
        try { block() } catch (e: SovariaException) { error.value = e.message }
    }

    fun signIn(email: String, password: String, create: Boolean) = run {
        if (create) sovaria.auth.signUp(email, password) else sovaria.auth.signInWithPassword(email, password)
    }

    // Rows come back keyed by column name. The row rules add "where owner_id = auth.uid()".
    private suspend fun load() { notes.value = sovaria.db.sql("select id, body from notes order by id desc").rows }

    fun add(body: String) = run {
        sovaria.db.sql("insert into notes (body) values (\$1)", listOf(body))
        load()
    }

    fun signOut() = run { sovaria.auth.signOut() }
}

#4. The screens

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { MaterialTheme { NotesScreen() } }
    }
}

@Composable
fun NotesScreen(vm: NotesViewModel = viewModel()) {
    val signedIn by vm.signedIn.collectAsState()
    val notes by vm.notes.collectAsState()
    val error by vm.error.collectAsState()
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var body by remember { mutableStateOf("") }

    Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        if (!signedIn) {
            OutlinedTextField(email, { email = it }, label = { Text("Email") })
            OutlinedTextField(password, { password = it }, label = { Text("Password") },
                visualTransformation = PasswordVisualTransformation())
            Button({ vm.signIn(email, password, create = false) }) { Text("Sign in") }
            TextButton({ vm.signIn(email, password, create = true) }) { Text("Create account") }
        } else {
            Row {
                OutlinedTextField(body, { body = it }, Modifier.weight(1f), placeholder = { Text("Write a note") })
                Button({ vm.add(body); body = "" }) { Text("Add") }
            }
            LazyColumn(Modifier.weight(1f)) { items(notes) { Text(it["body"]?.toString() ?: "") } }
            TextButton({ vm.signOut() }) { Text("Sign out") }
        }
        if (error.isNotEmpty()) Text(error, color = MaterialTheme.colorScheme.error)
    }
}

In Kotlin strings $ starts a template, so the SQL placeholder is written \$1.

#5. Run it

Press Run with an emulator or device. Create an account, add a note, sign out and back in: the note is still there, and a second account never sees it.

#Next steps

#Coming from Firebase or Supabase?

PlatformWhat this is called there
FirebaseFirebase Android (Kotlin) setup
SupabaseSupabase Kotlin quickstart
SovariaQuickstart: Android (Jetpack Compose)

Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.