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 Firebase push strategy layer: connect your existing FCM 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.Activity
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
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)
registerActivityOpenTracking(context.applicationContext)
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 registerActivityOpenTracking(appContext: Context) {
val app = appContext as? Application ?: return
app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
trackOpenFromIntent(activity.intent)
}
override fun onActivityNewIntent(activity: Activity, intent: Intent) {
trackOpenFromIntent(intent)
}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
})
}
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
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)
}
}Built into `Pushbrain.kt` — `init()` registers activity lifecycle hooks so tray taps fire the open beacon automatically. Custom launcher flows can still call `Pushbrain.trackOpenFromIntent(intent)` from `MainActivity.onCreate` / `onNewIntent`.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Pushbrain.trackOpenFromIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Pushbrain.trackOpenFromIntent(intent)
}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, then send a notification from the form below. Android does not auto-display FCM notifications while the app is in the foreground.
Hand this prompt to an AI coding agent already working in your codebase — it detects your platform and wires up the SDK itself, using this app's real sdkKey and API base.
You are integrating push notifications using Pushbrain (a bring-your-own-Firebase push notification service) into this codebase.
Context:
- Pushbrain sdkKey: pb_YOUR_KEY_HERE (public, safe to embed in client code — this is not a secret)
- Pushbrain API base: /api
- Device registration endpoint: POST /api/sdk/register-token
Task:
1. Detect this project's platform (web/React, Android/Kotlin, Flutter, or React Native) from its existing dependencies and file structure.
2. Install the matching Pushbrain SDK:
- Web: npm install @pushbrain/sdk firebase
- React Native: npm install pushbrain-react-native @react-native-firebase/app @react-native-firebase/messaging
- Android/Flutter: no native SDK package yet — call the registration endpoint above directly (POST platform, fcmToken, sdkKey) after obtaining an FCM token via the Firebase SDK already in the project.
3. Call pushbrain.init({ sdkKey: 'pb_YOUR_KEY_HERE', ... }) (web/React Native) from the app's startup code, using the project's OWN existing Firebase config — do not invent new Firebase credentials.
4. Do NOT hardcode a different sdkKey than the one above. Do NOT create a new Firebase project for this — if the project has no Firebase project connected yet, stop and ask the user to connect one first rather than guessing.
5. After wiring it up, ask the user to confirm the Pushbrain dashboard shows a new device registered — that confirmation step needs a human with dashboard access, not the agent.
Guardrails:
- The sdkKey above is meant to be public/client-embedded — do not confuse it with an API key (prefixed pb_sk_), which is a server-side secret and must never appear in client code.
- If the project has no notification-permission flow yet, add one before calling init() — init() requests permission itself, but the app should handle a denied result gracefully, not crash.