Cloud Messaging

Push notifications for your Flutter and React Native users via FCM — device tokens come from a native module, so this is not available in the browser; the web uses Web Push through your own service worker. Devices register themselves from your app; your backend or a Koolbase Function sends.

How it works

  1. Your app obtains an FCM token using firebase_messaging
  2. The SDK registers the token with Koolbase — tied to a stable device ID, using the publishable key
  3. Your backend (or a Koolbase Function, or the dashboard broadcast UI) sends with a secret API key
  4. Koolbase authenticates to FCM's HTTP v1 API and delivers to Android and iOS

Why sends require a secret key

The publishable key ships inside your app binary and is trivially extractable from any APK or IPA. If it could send, anyone could push arbitrary notifications to every device of your project. Push is therefore server-initiated only: sends authenticate with a kb_live_ key (write scope) that never leaves your backend. Send requests carrying a publishable key answer 401.

Setup

Delivery goes through FCM's HTTP v1 API, which authenticates with short-lived OAuth tokens minted from a Firebase service account. Koolbase handles the minting and caching; you provide the service account once.

  1. Firebase Console → your project → ⚙️ Project settings → Service accounts → Generate new private key. A JSON file downloads.
  2. Koolbase dashboard → your project → Secrets → create a secret named exactly FCM_SERVICE_ACCOUNT.
  3. Paste the entire JSON file as the value — from the opening { to the closing }, unmodified. Don't extract fields, don't strip the \ns inside the private key.

Upgrading from FCM_SERVER_KEY

Earlier versions used a legacy FCM server key stored as FCM_SERVER_KEY. Google removed the legacy API in June 2024, and that path no longer exists in Koolbase. Replace it with the FCM_SERVICE_ACCOUNT secret above — the old secret is ignored and can be deleted.

Rotation is instant

Secrets are encrypted at rest, and the messaging pipeline detects a changed service account on the next send — rotate the key in Firebase, update the secret, and the new credential takes effect with no restart.

iOS requires an APNs key — or sends silently don't arrive

FCM delivers to iOS by forwarding to Apple's APNs, so your Firebase project needs an APNs authentication key. Without it, sends to iOS devices succeed at the API (200) but never reach the device — the most confusing failure mode there is, because nothing errors.

In the Apple Developer account → Keys → create a key with the Apple Push Notifications service (APNs) capability, and download the .p8 file. Then in Firebase Console → Project settings → Cloud Messaging → your iOS app → upload it under APNs Authentication Key (you'll also need your Key ID and Team ID). Android needs none of this.

Use an APNs key (.p8), not a certificate — and why iOS may show sent:1 but no banner

Two iOS-only failure modes both look like a successful send (sent: 1) with nothing on screen:

Wrong environment. A debug build registers a sandbox APNs token; a release/TestFlight build registers a production one. An APNs Auth Key (.p8) works for both, so FCM routes automatically — a production certificate won't deliver to a sandbox token. Use the .p8 key (above), not a certificate, and this resolves itself.

Silent payload. Koolbase sets an explicit APNs alert (title + body + sound) so iOS presents the banner automatically — you don't need to build a local notification for a standard title/body send. If you're testing on a debug build and see no banner, it's almost always the environment/key mismatch above, not the payload.

Flutter SDK — register devices

Install firebase_messaging, then register the FCM token after initialization. Pass the signed-in user's id to target notifications per user later. Messaging is on by default; setting messagingEnabled: false in KoolbaseConfig disables the client entirely.

// After Firebase is initialized
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken != null) {
  await Koolbase.messaging.registerToken(
    token: fcmToken,
    platform: 'android', // or 'ios'
    userId: currentUser?.id,
  );
}

// Tokens rotate — keep the registration fresh:
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
  Koolbase.messaging.registerToken(token: newToken, platform: 'android');
});

Registration is idempotent per device: re-registering updates the stored token rather than duplicating it.

React Native SDK — register devices

import messaging from '@react-native-firebase/messaging';

const fcmToken = await messaging().getToken();
await Koolbase.messaging.registerToken({
  token: fcmToken,
  platform: 'android', // or 'ios'
});

messaging().onTokenRefresh(async (newToken) => {
  await Koolbase.messaging.registerToken({
    token: newToken,
    platform: 'android',
  });
});

Send from your backend

Sends are a single POST with a secret key. project_id is required in the body — keys are organization-scoped, and the project must belong to the key's organization.

curl -X POST https://api.koolbase.com/v1/messaging/send \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer kb_live_..." \
            --data '{
              "project_id": "your-project-id",
              "token": "the-device-fcm-token",
              "title": "Order shipped",
              "body": "Your order #1042 is on its way",
              "data": { "order_id": "1042" }
            }'

To notify a user rather than a specific device, send user_id instead of token (exactly one is required). Koolbase resolves all of that user's registered devices, sends to each, and prunes dead tokens — so your backend or Function never handles raw device tokens:

curl -X POST https://api.koolbase.com/v1/messaging/send \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer kb_live_..." \
              --data '{
                "project_id": "your-project-id",
                "user_id": "the-project-user-id",
                "title": "Order shipped",
                "body": "Your order #1042 is on its way"
              }'
            # Response: { "sent": 1, "failed": 0, "pruned": 0, "devices": 1 }

A user with no registered devices is not an error

If the user has no devices registered (never granted permission, or uninstalled), the send returns 200 with devices: 0 — not an error. A cron Function notifying many users won't crash on the ones who can't receive.

Register tokens after login

A device token is bound to a user via the session it was registered under. If your app calls registerToken before the user is signed in, the token is stored without an owner and user_id-targeted sends won't find it. Register (or re-register) once the user is authenticated.

The recommended pattern for app-driven notifications is a Koolbase Function: a database trigger or invoked Function runs server-side with access to your secrets, so it can send without any credential ever reaching the client.

Koolbase.messaging.send() is removed

Earlier SDK versions exposed a client-side send() method authenticated with the publishable key. It is rejected by the API for the security reason above and removed from the SDK. Move sends to your backend or a Function.

Responses and errors

200Delivered to FCM.
400Caller error, named in the message: invalid FCM registration token, missing field, or missing/malformed FCM_SERVICE_ACCOUNT secret.
401No usable credential — sends require a secret kb_live_ key; the publishable key is rejected by design.
403insufficient_scope: the key is read-only. Sending requires write scope.
404The project does not exist in this key’s organization. Cross-org requests answer not-found, never confirming what exists elsewhere.
410The device token was valid once but is no longer registered (app uninstalled, token rotated). Koolbase has already deleted it — stop sending to it.

Dead tokens are pruned automatically

When FCM reports a token as unregistered or invalid, Koolbase removes it from the device registry — on single sends (answered with 410) and during broadcasts (reported in the pruned count).

Dashboard broadcast

Send to all registered devices from the dashboard: project → Messaging → title, body, optional data → Broadcast. Broadcast authenticates with your dashboard session, not an API key.

  • Total registered devices — Android and iOS count
  • Sent today / Failed today — delivery stats
  • Recent sends log — status per message
  • Broadcast to all — one-click send to every registered token, with pruned count

What Koolbase does not handle

  • Receiving messages in the app — use firebase_messaging for that
  • Notification permissions — request via the platform SDK
  • Background message handlers — configure in firebase_messaging
  • Web push notifications — mobile only in v1

API reference

SurfaceDescription
Koolbase.messaging.registerToken(token, platform, userId?)Register an FCM device token (publishable key, from the app).
POST /v1/messaging/sendSend to one device (secret kb_live_ key, from your backend or a Function).
Dashboard → Messaging → BroadcastSend to all registered devices for the project.