Notify guides
10 · Android push (FCM)

Push to Android.

A Firebase project, a service account that can only send messages, and device tokens from your app. Notify does the sending, retries and pruning.


1

Add Firebase to the app

In the Firebase console, add an Android app with your package name, download google-services.json into the app module and add the com.google.gms.google-services Gradle plugin and firebase-messaging. That file only identifies the project to the app; it is not the credential Notify needs.


2

Create a dedicated service account

  1. In Google Cloud console for the same project: IAM & Admin › Service Accounts › Create, for example notify-sender.
  2. Give it one role: Firebase Cloud Messaging API Admin.
  3. Keys › Add key › JSON and download the file.

Do not upload the firebase-adminsdk-… account Firebase creates: it can do almost anything in the project, so a leak would be far worse. Notify accepts it with a warning. Google’s pages: authorizing send requests and creating a service account key.

upload-fcm.ts
import { readFileSync } from "node:fs";
import { Notify } from "@productcraft/notify";

const notify = new Notify();

// The key file of a service account that only has the
// "Firebase Cloud Messaging API Admin" role. Validated with Google first.
const credential = await notify.channels.fcm.uploadCredential({
  service_account: readFileSync("notify-sender-key.json", "utf8"),
});
console.log(credential.project_id, credential.client_email, credential.warnings);

3

Register tokens from the app

Register on every start and in onNewToken. app_id is the Firebase project id; on Android 13 and later, ask for the POST_NOTIFICATIONS permission first.

NotifyMessagingService.kt
class NotifyMessagingService : FirebaseMessagingService() {
    // Called when Firebase issues a new token (first start, reinstall, restore).
    override fun onNewToken(token: String) {
        CoroutineScope(Dispatchers.IO).launch { NotifyApi.registerDevice(token) }
    }
}

object NotifyApi {
    private val client = OkHttpClient()

    // Also call this on every app start with FirebaseMessaging.getInstance().token.
    suspend fun registerDevice(token: String) {
        val body = JSONObject()
            .put("channel", "fcm")
            .put("token", token)
            .put("app_id", "example-app-prod") // your Firebase project id
            .put("platform", "android")
            .put("timezone", TimeZone.getDefault().id)
            .toString()
            .toRequestBody("application/json".toMediaType())
        val req = Request.Builder()
            .url("https://api.notify.productcraft.co/v1/client/devices")
            .header("Authorization", "Bearer ${Session.notifyToken()}")
            .post(body)
            .build()
        client.newCall(req).execute().use { res ->
            val id = JSONObject(res.body!!.string()).getString("id")
            Prefs.notifyDeviceId = id // DELETE /v1/client/devices/{id} on logout
        }
    }
}

// Android 8+: create the channel your category maps to before the first push.
fun createChannels(context: Context) {
    val manager = context.getSystemService(NotificationManager::class.java)
    manager.createNotificationChannel(NotificationChannel("orders", "Orders", NotificationManager.IMPORTANCE_DEFAULT))
}

Or from your backend, if the app already sends you its token:

register-from-server.ts
await notify.recipients.devices.create("user_123", {
  channel: "fcm",
  token: "dXN0b20…", // FirebaseMessaging.getInstance().token
  app_id: "example-app-prod", // your Firebase project id
  platform: "android",
});

4

Notification channels

Android 8 and later shows a push only in a notification channel the app created. Notify sends each category’s pushes to the channel with the category’s key, or the android_channel_id you set on the category. Create those channels at app start, as in the Kotlin above. Critical types go out with high priority.

channel.ts
// Pushes of this category use the Android channel "orders"
// (the app must create it; without android_channel_id the category key is used).
const orders = (await notify.categories.list().toArray({ maxItems: 200 })).find((c) => c.key === "orders");
if (orders) await notify.categories.update(orders.id, { android_channel_id: "orders" });

5

What happens after a send

  • Google accepts it: sent, with the FCM message name on the delivery.
  • UNREGISTERED or SENDER_ID_MISMATCH: the device is pruned. Tokens unused for about 270 days expire at Google, and Notify prunes them too.
  • The service account is deleted or loses its role: credential_error, Android push pauses, owners are emailed once and a credential.invalid webhook fires.