Push Notifications

Firebase Cloud Messaging setup, token registration, and signed-in test pushes.

The Kotlin template uses Firebase Cloud Messaging for Android push, plus a Supabase-backed device-token table and a signed-in Edge Function for smoke tests. The app builds without Firebase configured; FCM activates when app/google-services.json is present and a signed-in user registers a token.

What it provides

SurfaceStatusImplementation
FCM receive serviceIntegratedFcmService extends FirebaseMessagingService
Runtime permissionIntegratedNotificationsScreen requests POST_NOTIFICATIONS on Android 13+
Token registrationIntegratedNotificationsRepository upserts to public.device_tokens
In-app inboxExampleIncoming messages are kept in an in-memory StateFlow, capped at 50
Test senderIntegratedsend-test-push sends through FCM HTTP v1 using Supabase Edge secrets

FCM needs Google Play services. It works on real Android devices and on emulator images that include Play services. You do not need a paid Play developer account for local FCM testing.

Key files

Setup

Create a Firebase project

Create or reuse a Firebase project for your Android app. Add the Android package name from app/build.gradle.kts; the template default is:

com.soarstarter.kotlin

Add google-services.json

Download the Android client config from Firebase Console and place it at:

soar-kotlin/app/google-services.json

app/build.gradle.kts applies com.google.gms.google-services only when that file exists, so CI and zero-key local builds still compile without Firebase. The template README mentions app/google-services.json.example, but the current template tree does not include that example file; use the JSON downloaded from Firebase as the source of truth.

Apply the device-token migration

Apply supabase/migrations/20260508000001_create_device_tokens.sql. The table stores user_id, token, platform = android, provider = fcm, and timestamps. RLS lets users select, insert, update, and delete only their own rows.

Deploy send-test-push

Deploy the signed-in test sender:

supabase functions deploy send-test-push --project-ref <ref>

supabase/config.toml keeps verify_jwt = true for this function because the Android app calls it as the current Supabase user.

Add Firebase Admin secrets

In Firebase Console, generate a service-account private key, then store it as a Supabase Edge secret:

supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='<service-account-json>' --project-ref <ref>

If multi-line JSON is awkward in your shell, use the split secrets instead: FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, and FIREBASE_PRIVATE_KEY.

Token pipeline

Firebase token
  -> FcmService.onNewToken() or NotificationsRepository.refreshAndRegisterToken()
  -> NotificationsRepository.registerTokenOrThrow()
  -> SupabaseDeviceTokenRemoteDataSource.register()
  -> upsert public.device_tokens on (user_id, token)

NotificationsScreen owns the explicit enable flow. On Android 13 and newer it asks for Manifest.permission.POST_NOTIFICATIONS; on older versions it skips the runtime permission and requests the Firebase token directly.

SupabaseDeviceTokenRemoteDataSource uses PostgREST upsert with:

onConflict = "user_id,token"
defaultToNull = false

The migration has a matching unique constraint on (user_id, token), so re-registering the same token refreshes updated_at and last_seen_at instead of creating duplicates.

Current code registers tokens on explicit enable and when Firebase rotates a token. It does not delete device_tokens on sign-out. Because the Edge Function reads through the caller's RLS, a signed-out user cannot send a test push, but old token rows can remain until you add explicit cleanup.

Receiving and opening pushes

FcmService.onMessageReceived() accepts either the FCM notification payload or data keys:

{
  "title": "Soar Starter test push",
  "body": "Your Android FCM setup is working.",
  "url": "soar://notifications"
}

The service writes an AppNotification into the in-memory inbox and posts a system notification on channel default. If the payload contains url, the notification tap starts MainActivity with ACTION_VIEW and that URI. From there, DeepLinkHandler forwards the URI through the normal deep-link resolver.

The inbox is intentionally simple sample state: it keeps the newest 50 notifications for the current process and clears when the process dies or the user taps Clear.

Test with the app

  1. Use a real device or a Play-services emulator image.
  2. Sign in to the app.
  3. Open Me -> Notifications.
  4. Grant POST_NOTIFICATIONS when Android asks.
  5. Tap Enable push notifications.
  6. Tap Send test push.
  7. Confirm the system notification appears and the in-app inbox gets a row.

NotificationsRepository.sendTestPush() refuses to send until a user is signed in and a push token has been registered.

Test with curl

Use a signed-in Supabase access token from the same account that registered the device token:

curl -i \
  -X POST https://<ref>.supabase.co/functions/v1/send-test-push \
  -H "Authorization: Bearer <supabase-access-token>" \
  -H "Content-Type: application/json" \
  --data '{
    "title": "Soar Starter test push",
    "body": "Your Android FCM setup is working.",
    "url": "soar://notifications"
  }'

The function never accepts a device token in the request body. It validates the caller, reads that user's platform = android and provider = fcm rows through RLS, and sends with server-held Firebase Admin credentials.

Customize

  • Replace the in-memory inbox with a persisted table if notifications are a product feature.
  • Add token cleanup on sign-out if you do not want old rows retained.
  • Add topic, segment, or campaign metadata to device_tokens only after extending the RLS policies.
  • Keep Firebase Admin credentials in Supabase Edge secrets, never in the APK.

On this page