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
- In Google Cloud console for the same project: IAM & Admin › Service Accounts › Create, for example
notify-sender. - Give it one role: Firebase Cloud Messaging API Admin.
- 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.
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.
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:
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.
// 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. UNREGISTEREDorSENDER_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 acredential.invalidwebhook fires.