Documentation
Guides
Push Notifications

Push Notifications Guide

Complete guide to sending push notifications with Moontraze.


Overview

Moontraze uses your own Firebase project for push notifications — this is called BYOF (Bring Your Own Firebase).

Why BYOF?

  • Your Firebase project, your data, your rules
  • No shared rate limits with other developers
  • Complete isolation — your notifications never mix with others
  • You control Firebase settings, quotas, and analytics
  • Moontraze never touches your Firebase data

Cost: 100% free — FCM has no message limits.

Roles:

  • Owner — Can configure Firebase, send and receive
  • User — Can only receive

Architecture

┌──────────────────────────────────────────────┐
│          YOUR Firebase Project                │
│  ├── Android app (com.your.app)              │
│  │   └── google-services.json → Flutter app  │
│  └── Service Account JSON → uploaded once    │
│       to Moontraze (encrypted at rest)       │
└──────────────────────────────────────────────┘
                    │
                    ▼
┌──────────────────────────────────────────────┐
│            Moontraze Backend                  │
│  Stores encrypted credentials per project     │
│  Sends push via YOUR Firebase on request      │
└──────────────────────────────────────────────┘
                    │
                    ▼
┌──────────────────────────────────────────────┐
│           Your Users' Devices                 │
│  Receive push via YOUR Firebase              │
└──────────────────────────────────────────────┘

Setup

Step 1: Create a Firebase Project

  1. Go to Firebase Console (opens in a new tab)
  2. Click Add project → give it a name (e.g. my-app-prod)
  3. Google Analytics is optional — you can skip it
  4. Click Create project

Note: Free Spark plan is enough. No paid tier required.


Step 2: Add Your Android App

  1. In your Firebase project → click Add app → choose Android
  2. Enter your Android package name — must match your Flutter app's applicationId exactly
    • Example: com.yourcompany.yourapp
  3. (Optional) Add SHA-1 for Google Sign-In
  4. Click Register app
  5. Download google-services.json
  6. Place it at: android/app/google-services.json in your Flutter project

⚠️ Critical: The package_name inside google-services.json MUST exactly match your app's applicationId in android/app/build.gradle.kts. Mismatch will crash the app at Firebase.initializeApp().


Step 3: Generate Service Account Key

  1. In Firebase Console → click ⚙️ Project Settings
  2. Go to Service Accounts tab
  3. Click Generate new private key
  4. Confirm → a JSON file downloads

⚠️ Keep it safe! Never commit this file to Git. Never share it publicly. It grants full admin access to your Firebase project.


Step 4: Upload Service Account to Moontraze

  1. Open Moontraze Dashboard → your project
  2. Go to Notification Setup tab
  3. Upload the service account JSON file
  4. Moontraze encrypts it with AES-256-GCM and stores it securely
  5. A verification runs automatically — you'll see a green "Firebase Connected" status

Note: If verification fails, check that your service account has the Firebase Cloud Messaging API enabled in Google Cloud Console.


Step 5: Enable Push Notifications

In Moontraze Dashboard → Notifications tab → toggle Enable FCM to ON.


Client Integration

Flutter Setup

Add Firebase to your app:

# pubspec.yaml
dependencies:
  firebase_core: ^3.6.0
  firebase_messaging: ^15.1.3
  moontraze_flutter: ^1.0.0

Android Configuration

android/build.gradle.kts (project level):

plugins {
    // ... existing plugins
    id("com.google.gms.google-services") version "4.4.2" apply false
}

android/app/build.gradle.kts (app level):

plugins {
    id("com.android.application")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
    id("com.google.gms.google-services")   // ✅ Add this
}
 
android {
    defaultConfig {
        minSdk = 21   // FCM requires API 21+
    }
}

android/app/src/main/AndroidManifest.xml (inside <application>):

<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="high_importance_channel" />

Permissions (before <application>):

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.VIBRATE"/>

Flutter Code

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:moontraze_flutter/moontraze_flutter.dart';
 
// Top-level background handler
@pragma('vm:entry-point')
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background notification: ${message.notification?.title}');
}
 
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  // 1. Firebase init (reads google-services.json on Android)
  await Firebase.initializeApp();
 
  // 2. Register background handler
  FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
 
  // 3. Initialize MoonSDK
  final moon = MoonSDK(MoonConfig(
    projectId: 'YOUR_PROJECT_ID',
    apiKey: 'YOUR_API_KEY',
  ));
 
  // 4. Request notification permission
  final settings = await FirebaseMessaging.instance.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );
 
  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    // 5. Get FCM token
    final token = await FirebaseMessaging.instance.getToken();
 
    if (token != null) {
      // 6. Register token with Moontraze
      await moon.notifications.registerToken(
        token,
        deviceInfo: {'platform': 'android'},
      );
    }
 
    // 7. Handle token refresh
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
      await moon.notifications.registerToken(
        newToken,
        deviceInfo: {'platform': 'android'},
      );
    });
  }
 
  // 8. Foreground message handler
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    print('Foreground: ${message.notification?.title}');
    // Show local notification with flutter_local_notifications if needed
  });
 
  // 9. Notification tap handler
  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    final data = message.data;
    // Deep link based on data['type'], data['postId'], etc.
  });
 
  runApp(MyApp());
}

Logout — Unregister Token

await moon.notifications.unregisterToken(currentToken);

React Native Setup

npm install @react-native-firebase/app @react-native-firebase/messaging

Place google-services.json in:

android/app/google-services.json

android/build.gradle:

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.4.2'
    }
}

android/app/build.gradle:

apply plugin: 'com.google.gms.google-services'

Code

import messaging from '@react-native-firebase/messaging';
import MoonSDK from 'moontraze-sdk';
 
// Register background handler (must be outside component)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  console.log('Background message:', remoteMessage);
});
 
async function setupNotifications() {
  const moon = new MoonSDK({
    projectId: 'YOUR_PROJECT_ID',
    apiKey: 'YOUR_API_KEY',
  });
 
  // Request permission
  const authStatus = await messaging().requestPermission();
  const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;
 
  if (!enabled) return;
 
  // Get token
  const token = await messaging().getToken();
 
  // Register with Moontraze
  await moon.notifications.registerToken(token, {
    platform: 'android',
  });
 
  // Handle token refresh
  messaging().onTokenRefresh(async (newToken) => {
    await moon.notifications.registerToken(newToken, {
      platform: 'android',
    });
  });
 
  // Foreground handler
  messaging().onMessage(async (remoteMessage) => {
    console.log('Foreground:', remoteMessage.notification?.title);
  });
 
  // Tap handler
  messaging().onNotificationOpenedApp((remoteMessage) => {
    const data = remoteMessage.data;
    // Navigate based on data.type
  });
}

Sending Notifications

All send methods require a projectId. Notifications are delivered via your Firebase project.

Send to All Users in a Project

await moon.notifications.sendToProject({
  projectId: 'YOUR_PROJECT_ID',
  title: 'New Feature Released',
  body: 'Check out what we just shipped!',
});

Send to a Specific User

await moon.notifications.sendToUser({
  projectId: 'YOUR_PROJECT_ID',
  userId: 'USER_ID',
  title: 'Order Shipped',
  body: 'Your order is on the way',
});

Send to a Topic

Topics are useful for segmented audiences (e.g. premium_users, beta_testers).

await moon.notifications.sendToTopic({
  projectId: 'YOUR_PROJECT_ID',
  topic: 'premium_users',
  title: 'Premium Feature',
  body: 'Exclusive for premium users',
});

Note: Devices must subscribe to the topic using FirebaseMessaging.instance.subscribeToTopic('premium_users').

Send with Custom Data (Deep Links, Images, etc.)

The data field is delivered to the client and can carry any payload (FCM 4KB limit).

await moon.notifications.sendToUser({
  projectId: 'YOUR_PROJECT_ID',
  userId: 'USER_ID',
  title: 'New comment on your post',
  body: 'Sarah replied to your post',
  data: {
    type: 'post',                          // ✅ for tap routing
    postId: 'clx123abc',
    commentId: 'clx456def',
    imageUrl: 'https://cdn.example.com/post.jpg',  // ✅ for rich notification
    deepLink: 'myapp://post/clx123abc',
  },
});

Client side:

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  final data = message.data;
  if (data['type'] == 'post') {
    Navigator.pushNamed(context, '/post', arguments: data['postId']);
  }
});

Schedule for Later

await moon.notifications.schedule({
  projectId: 'YOUR_PROJECT_ID',
  userId: 'USER_ID',       // optional — omit for project-wide
  title: 'Reminder',
  body: "Don't forget!",
  sendAt: new Date('2026-12-31T10:00:00Z'),
});

History

const history = await moon.notifications.getHistory({
  projectId: 'YOUR_PROJECT_ID',
  limit: 50,
});

Each entry contains:

  • title, body, data
  • status (sent | failed | scheduled)
  • successCount, failureCount
  • createdAt

Delete History

// Delete a single notification
await moon.notifications.deleteHistory('NOTIFICATION_ID');
 
// Clear all history for a project
await moon.notifications.clearHistory({ projectId: 'YOUR_PROJECT_ID' });

Preferences

User notification preferences (per user, applies across all projects):

const prefs = await moon.notifications.getPreferences();
 
await moon.notifications.updatePreferences({
  pushEnabled: true,
  emailEnabled: false,
  marketingEnabled: false,
  securityEnabled: true,
  quietHoursEnabled: true,
  quietHoursStart: '22:00',
  quietHoursEnd: '08:00',
  timezone: 'Asia/Kolkata',
});

Note: Moontraze stores preferences but does not automatically apply quiet hours — your backend must respect them when sending.


REST API (Alternative to SDK)

If you prefer HTTP directly:

Register Token

POST /api/notifications/register
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "token": "FCM_TOKEN",
  "projectId": "YOUR_PROJECT_ID",
  "deviceInfo": {
    "platform": "android",
    "deviceId": "unique-device-id"
  }
}

Unregister Token

POST /api/notifications/unregister
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "token": "FCM_TOKEN"
}

Send to User

POST /api/notifications/send-to-user/USER_ID
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "projectId": "YOUR_PROJECT_ID",
  "title": "New comment",
  "body": "Someone commented on your post",
  "data": {
    "type": "comment",
    "postId": "abc123"
  }
}

Send to Project (All Users)

POST /api/notifications/send-to-project/YOUR_PROJECT_ID
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "title": "Announcement",
  "body": "We just launched feature X"
}

Send to Topic

POST /api/notifications/send-to-topic
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "projectId": "YOUR_PROJECT_ID",
  "topic": "premium_users",
  "title": "Premium only",
  "body": "Exclusive content"
}

Schedule

POST /api/notifications/schedule
Authorization: Bearer <jwt>
Content-Type: application/json
 
{
  "projectId": "YOUR_PROJECT_ID",
  "title": "Reminder",
  "body": "Don't forget!",
  "sendAt": "2026-12-31T10:00:00.000Z"
}

Get History

GET /api/notifications/history?projectId=YOUR_PROJECT_ID&limit=50
Authorization: Bearer <jwt>

Delete History

# Single
DELETE /api/notifications/history/NOTIFICATION_ID
Authorization: Bearer <jwt>
 
# All
DELETE /api/notifications/history?projectId=YOUR_PROJECT_ID
Authorization: Bearer <jwt>

Error Responses

Firebase Not Configured

{
  "error": "Firebase not configured for this project. Please upload your service account credentials in Project Settings → Notifications.",
  "code": "FIREBASE_NOT_CONFIGURED"
}

Fix: Upload service account JSON in Notification Setup tab.

Invalid Service Account

{
  "error": "Failed to decrypt Firebase credentials for project xxx. Please re-upload your service account JSON."
}

Fix: Re-upload the service account JSON. This usually means the ENCRYPTION_SECRET was rotated on the server.

Invalid Token

Moontraze auto-deactivates tokens that Firebase reports as invalid. No action needed — the client will get a new token via onTokenRefresh.


Best Practices

  • Request permission only when needed — Explain why before prompting
  • Handle token refresh — Tokens rotate periodically; always register the new one
  • Unregister on logout — Prevents sending to the wrong user
  • Don't spam users — Respect frequency and relevance
  • Use data for routing, not UI — Keep payload small (FCM 4KB limit)
  • Meaningful titles/bodies — Truncated at ~50 chars on lock screen
  • Test on real devices — Emulators can miss FCM delivery
  • Respect quiet hours — Check user preferences before sending
  • Never commit service account JSON — Use environment variables on your backend
  • Rotate service account keys — Every 90 days as security best practice

Troubleshooting

Notifications not arriving

Checklist:

  1. google-services.json present in android/app/ and package_name matches applicationId?
  2. Firebase credentials uploaded to Moontraze (green status in Notification Setup)?
  3. FCM enabled in Moontraze Notifications tab?
  4. Token registered successfully? (check logs for [FCM] Register response: 200)
  5. Test on real device (emulator limitations)
  6. Check Firebase Console → Cloud Messaging → Reports for delivery stats

No matching client found for package name

Cause: google-services.json me package_name aur app ka applicationId match nahi kar raha.

Fix:

  1. Firebase Console → Add correct Android app with your package name
  2. Download new google-services.json
  3. Replace in android/app/

FIREBASE_NOT_CONFIGURED from API

Cause: Project me service account JSON upload nahi hua.

Fix: Dashboard → Notification Setup → Upload JSON.

Notification received on Android but not iOS

Cause: iOS requires APNs certificate/key configured in Firebase Console.

Fix:

  1. Firebase Console → Project Settings → Cloud Messaging
  2. Upload APNs authentication key (.p8) from Apple Developer
  3. Enable Push Notifications capability in Xcode

Cost

Firebase Cloud Messaging (FCM) is 100% free and unlimited.

  • No per-message charges
  • No monthly quota
  • No user limits
  • No topic limits (except ~2000 subscriptions per device)

This is true for the Spark (free) plan — you do NOT need Blaze or any paid plan for FCM.


Support


Last updated: 2026-09-22