Android Backend for Kotlin Apps: Postgres, Auth and Push

An Android backend needs a database you can query from coroutines, sign-in that survives process death, push through Firebase Cloud Messaging and a server that checks Google Play purchases. With StitchDB, one project provides Postgres, Auth, Storage, Realtime, Edge Functions, push and purchase verification, and the Kotlin Multiplatform SDK reaches it from Android, the JVM and iOS. This guide sets up an Android app, step by step, with code from the SDK docs.

#Why a hosted Postgres backend for Android

Most Android apps start with a choice between a document store and writing an API in front of a SQL database. A Postgres backend with a generated REST API removes the API work without giving up SQL:

  • Tables, joins, constraints and transactions, the way a relational model expects.
  • Authorization written once, as Row Level Security policies, and enforced for every client.
  • Plain Postgres underneath, so you can connect psql, a BI tool or an ORM on the JVM and take a pg_dump with you if you ever leave.

The Kotlin SDK maps rows to @Serializable data classes and exposes realtime as Flows, so it fits a modern coroutine-based app.

#1. Gradle setup

The SDK is built from source as a Gradle included build. Clone it next to your app:

Terminal
git clone https://github.com/stitchdb/stitchdb-kotlin
kotlin
// settings.gradle.kts
includeBuild("../stitchdb-kotlin") {
    dependencySubstitution {
        for (m in listOf("stitchdb-kt", "auth-kt", "postgrest-kt", "storage-kt", "realtime-kt", "functions-kt"))
            substitute(module("com.stitchdb:$m")).using(project(":$m"))
    }
}
kotlin
// build.gradle.kts
plugins { kotlin("plugin.serialization") version "2.3.21" }

dependencies {
    implementation("com.stitchdb:postgrest-kt:1.4.0")
    implementation("com.stitchdb:auth-kt:1.4.0")
    implementation("com.stitchdb:storage-kt:1.4.0")
    implementation("com.stitchdb:realtime-kt:1.4.0")
    implementation("com.stitchdb:functions-kt:1.4.0")
    implementation("io.ktor:ktor-client-okhttp:3.4.3") // Android/JVM; ktor-client-darwin on iOS, ktor-client-cio anywhere
}

You need Kotlin 2.x with coroutines and kotlinx.serialization, plus one Ktor engine. OkHttp is the usual choice on Android.

#2. Create the client

kotlin
import com.stitchdb.createStitchClient
import com.stitchdb.auth.Auth
import com.stitchdb.postgrest.Postgrest
import com.stitchdb.storage.Storage
import com.stitchdb.realtime.Realtime
import com.stitchdb.functions.Functions

val stitchdb = createStitchClient(
    stitchdbUrl = "https://<project-ref>.p.stitchdb.com",
    stitchdbKey = "<anon-key>",
) {
    install(Auth)
    install(Postgrest)
    install(Storage)
    install(Realtime)
    install(Functions)
}

Install only the plugins you use. The session is saved on the device, so users stay signed in between app starts. Keep one instance, for example in your Application class or provided by Hilt or Koin.

Only the anon key belongs in the APK. Anything in an APK can be decompiled; the anon key is built for that, and Row Level Security limits it. Put the URL and key in BuildConfig fields from local.properties or CI secrets so they are not committed, but do not mistake that for protection.

#3. Schema and policy

SQL
create table public.todos (
  id bigint generated always as identity primary key,
  user_id uuid not null default auth.uid() references auth.users on delete cascade,
  task text not null,
  is_complete boolean not null default false,
  created_at timestamptz not null default now()
);

alter table public.todos enable row level security;

create policy "Users manage their own todos" on public.todos
  for all to authenticated
  using (user_id = auth.uid())
  with check (user_id = auth.uid());

#4. Queries with data classes

kotlin
import com.stitchdb.postgrest.from
import com.stitchdb.postgrest.query.Columns
import com.stitchdb.postgrest.query.Order
import kotlinx.serialization.Serializable

@Serializable
data class Todo(val id: Long? = null, val task: String, val is_complete: Boolean = false)

// Select with filters, order and paging
val todos = stitchdb.from("todos").select(columns = Columns.list("id", "task", "is_complete")) {
    filter { eq("is_complete", false) }
    order("created_at", Order.DESCENDING)
    range(0L..9L)
}.decodeList<Todo>()

// Insert (returning the row), update, delete
val created = stitchdb.from("todos").insert(Todo(task = "Ship it")) { select() }.decodeSingle<Todo>()
stitchdb.from("todos").update({ set("is_complete", true) }) { filter { eq("id", created.id!!) } }
stitchdb.from("todos").delete { filter { eq("id", 1) } }

These are suspend calls; run them from a viewModelScope.launch { } and expose the result as StateFlow. For data classes that match your schema exactly, generate them:

Terminal
stitchdb gen types kotlin --linked --package com.example.db -o app/src/main/kotlin/DatabaseTypes.kt

The generated file has @Serializable classes per table, an insert class per table and enums. Column names become camelCase and map back when encoding.

#5. Sign-in on Android

kotlin
import com.stitchdb.auth.auth
import com.stitchdb.auth.providers.Github
import com.stitchdb.auth.providers.builtin.Email
import com.stitchdb.auth.providers.builtin.OTP

stitchdb.auth.signUpWith(Email) { email = "[email protected]"; password = "a-strong-password" }
stitchdb.auth.signInWith(Email) { email = "[email protected]"; password = "a-strong-password" }
stitchdb.auth.signInWith(OTP) { email = "[email protected]" }   // magic link / code
stitchdb.auth.signInWith(Github)                              // opens the browser

OAuth and magic links come back through a deep link. Set scheme and host in install(Auth) { }, add a matching intent filter to your activity in AndroidManifest.xml, add the URL (for example myapp://login-callback) to the project's redirect URLs, and hand incoming intents to the SDK:

kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    stitchdb.handleDeeplinks(intent)
}

Or open the hosted sign-in page and skip writing auth screens:

kotlin
import com.stitchdb.auth.HostedPageView

stitchdb.auth.signInWithHostedPage(view = HostedPageView.SIGN_UP, lang = "de")

Observe the session as a Flow to switch screens:

kotlin
// SessionStatus.Authenticated, NotAuthenticated, Initializing, RefreshFailure
stitchdb.auth.sessionStatus.collect { status -> println(status) }

#6. Realtime as Flows

kotlin
import com.stitchdb.realtime.*

val changes = stitchdb.channel("todos-changes")
val inserts = changes.postgresChangeFlow<PostgresAction.Insert>(schema = "public") { table = "todos" }
inserts.onEach { println(it.record) }.launchIn(scope)
changes.subscribe()

Collect in a lifecycle-aware scope and call stitchdb.realtime.removeChannel(changes) when the screen goes away. Add the table to the stitchdb_realtime publication first; users only receive rows their policies allow (Postgres changes).

#7. Files

kotlin
import com.stitchdb.storage.storage
import kotlin.time.Duration.Companion.seconds

val bucket = stitchdb.storage.from("avatars")
bucket.upload("user-1/avatar.png", bytes) { upsert = true }
val publicUrl = bucket.publicUrl("user-1/avatar.png")                     // public buckets
val signedUrl = bucket.createSignedUrl("user-1/avatar.png", 60.seconds)  // private

Use the user id as the first folder and a storage policy that checks it. Details in the file uploads guide.

#8. Push through FCM

Upload your Firebase service account JSON in Studio › Integrations › Push. Get the FCM token in the app with Firebase Messaging, then register it after sign-in and in onNewToken:

kotlin
import com.stitchdb.push.*

stitchdb.push.register(PushDevice.android(fcmToken, topics = listOf("news")))
stitchdb.push.subscribe("sale")

val inbox = stitchdb.push.inbox(unread = true)            // Inbox(notifications, unread)
stitchdb.push.notifications().collect { showBanner(it.title) }   // live, needs the Realtime plugin

The SDK depends on no push library; it only stores the token. Sending happens with the service role from SQL, an Edge Function or your server, for example select push.notify(...) from a trigger. See Push notifications.

#9. Google Play Billing

kotlin
import com.stitchdb.purchases.PurchaseInput

stitchdb.purchases.sync(PurchaseInput.Google(purchaseToken = purchase.purchaseToken, productId = "pro_monthly"))
if (stitchdb.purchases.hasEntitlement("pro")) unlock()

Call sync in onPurchasesUpdated for every purchase in state PURCHASED. StitchDB checks it with Google using a service account you upload, and acknowledges it for you (Google refunds unacknowledged purchases after three days). Real-time developer notifications keep renewals and cancellations current. See In-app purchases.

#10. Crashes

kotlin
stitchdb.crashes.start(context, release = BuildConfig.VERSION_NAME, environment = "production")
// uncaught exceptions on any thread are written to disk and sent on the next start

Reports are grouped into issues per release and show the API call that failed before the crash. R8 mapping files and NDK crashes are not supported yet, so obfuscated stacks stay obfuscated for now. See crash reporting and analytics.

#Trade-offs

ConsiderationDetail
InstallationIncluded Gradle build from GitHub, not Maven Central
RegionsEU only (Germany, Finland)
Native crashesNDK and R8 mapping not supported yet
Lock-inLow: standard Postgres, full project export

#FAQ

#Does the Kotlin SDK work with Kotlin Multiplatform?

Yes. It targets Android, the JVM and iOS. Use ktor-client-darwin as the engine on iOS.

#Can I use Room for offline data?

You can, and sync it yourself. StitchDB also has offline sync, which keeps a local copy of chosen tables and queues writes; the Kotlin SDK exposes it as client.offline(...) with flows for its events.

#How do I run server-side Kotlin against the same project?

Create a client with the service_role key on your server, or connect over JDBC to the pooled Postgres URL. Server code bypasses Row Level Security, so check permissions yourself.

#What happens when a policy blocks a request?

The SDK throws a RestException (for the database, a PostgrestRestException with code, details and hint). Code 42501 means a policy or grant refused it.

Start building your Android backend today: sign up for free.