Pushbrain is a Firebase push strategy layer for indie developers and small SaaS teams — campaigns, audience health, and AI-assisted messaging on top of your existing FCM project, without vendor lock-in.
Pushbrain is a full Firebase push notification dashboard on top of FCM: connect your existing project once, then run campaigns, scheduling, AI copy, and audience health without building a notification backend from scratch.
See a real push notification arrive in this browser before connecting any Firebase project of your own. The token this creates is used once to send you one message, then discarded — nothing is stored.
Firebase Cloud Messaging is free, reliable transport. It is not a campaign tool — no scheduling UI, no token health dashboard, no AI copy, no A/B tests. Pushbrain fills that gap while your Firebase project remains the sender of record.
Pick your platform. Sign up to replace pb_YOUR_KEY_HERE with your own sdkKey.
Project-level `build.gradle.kts`:
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.gms.google-services")
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-messaging-ktx")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Debug-only if your API_BASE is plain http:// on Android 9+. Prefer HTTPS in production. -->
<application android:usesCleartextTraffic="true" ... />Call this from your first Activity before testing pushes.
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
private fun requestNotificationPermission() {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001)
}
}Create this file anywhere in your `app/src/main/java/...` package.
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import com.google.firebase.messaging.FirebaseMessaging
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
object Pushbrain {
private const val SDK_KEY = "pb_YOUR_KEY_HERE"
private const val API_BASE = "/api"
private val http = OkHttpClient()
fun init(context: Context) {
createNotificationChannel(context)
val installId = getOrCreateInstallId(context)
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token -> register(token, installId) }
.addOnFailureListener { e -> Log.w("Pushbrain", "getToken failed", e) }
}
/** Fires the open-rate beacon when a notification tap delivers trackUrl in intent extras. */
fun trackOpenFromIntent(intent: Intent?) {
if (intent == null) return
val trackUrl = intent.getStringExtra("trackUrl")
?: intent.getStringExtra("notifId")?.let { "$API_BASE/t/o?n=$it" }
if (trackUrl != null) fireOpenBeacon(trackUrl)
}
private fun fireOpenBeacon(trackUrl: String) {
http.newCall(Request.Builder().url(trackUrl).build()).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) = response.close()
})
}
/** Stable per-install id, persisted locally. Lets Pushbrain recognize this
* device again after its FCM token rotates instead of double-counting it. */
private fun getOrCreateInstallId(context: Context): String {
val prefs = context.getSharedPreferences("pushbrain", Context.MODE_PRIVATE)
return prefs.getString("install_id", null) ?: java.util.UUID.randomUUID().toString().also {
prefs.edit().putString("install_id", it).apply()
}
}
private fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channel = NotificationChannel(
"default",
"Default",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Pushbrain push notifications"
}
context.getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
}
private fun register(fcmToken: String, installId: String) {
val body = JSONObject().apply {
put("sdkKey", SDK_KEY)
put("fcmToken", fcmToken)
put("platform", "android")
put("installId", installId)
put("appVersion", BuildConfig.VERSION_NAME)
put("environment", if (BuildConfig.DEBUG) "local" else "production")
put("deviceLabel", "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
put("userEmail", currentUserEmailOrNull) // Optional: app user's email, if signed in
put("userName", currentUserNameOrNull) // Optional: app user's display name
// Optional location — enable Location collection in the dashboard first:
// put("city", currentUserCityOrNull)
// put("lat", currentUserLatOrNull)
// put("lng", currentUserLngOrNull)
put("permissionStatus", "granted")
put("timezone", java.util.TimeZone.getDefault().id)
put("sdkVersion", "android-snippet-1")
}.toString().toRequestBody("application/json".toMediaType())
http.newCall(
Request.Builder()
.url("$API_BASE/sdk/register-token")
.post(body)
.build()
).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: java.io.IOException) =
Log.w("Pushbrain", "register failed", e)
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
Log.d("Pushbrain", "registered: ${response.code}")
response.close()
}
})
}
}In your `Application` subclass (register it in `AndroidManifest.xml` as `android:name=".MyApp"`):
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Pushbrain.init(this)
}
}Open counts only increase when a user taps the notification. Forward launch intents from `MainActivity` so Android tray taps fire the open beacon. Opening the app icon manually does not count as a notification open.
import android.content.Intent
import android.os.Bundle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Pushbrain.trackOpenFromIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
Pushbrain.trackOpenFromIntent(intent)
}To power weather-gated Automations (Pro), enable Location collection in Integration and send city and/or lat/lng on registration. Preview weather for free with Integration → Test weather notification. Full guide: /docs/weather-notifications
Build and run on a phone, accept the notification permission prompt, and confirm the backend logs POST /sdk/register-token. Then refresh this dashboard — the device count ticks up within ~5 seconds. Background or lock the phone, send a notification, then tap the notification itself. Opening the app icon manually will not increase opens.
If you already store FCM tokens, your installed base can receive Pushbrain sends before they update. Connect the same Firebase project, import tokens via the HTTP API, then ship the SDK later for tracking and token refresh.
/sdk/register-token with sdkKey, fcmToken, and platform. Prefer recently active tokens (Android tokens older than ~270 days are often expired).curl -X POST /api/sdk/register-token \
-H 'Content-Type: application/json' \
-d '{"sdkKey":"pb_YOUR_KEY_HERE","fcmToken":"EXISTING_FCM_TOKEN","platform":"android"}'