Push Notifications Cross-Platform com Firebase: Guia 2026

RESUMO

Firebase Cloud Messaging – Notificações Push Cross-Platform

Implementação completa de push notifications para iOS e Android usando Firebase Cloud Messaging (FCM) com exemplos práticos e estratégias de engajamento.

Keywords: Firebase Cloud Messaging, Cross-Platform, Push Notifications


ÍNDICE

1 Fundamentos do Firebase Cloud Messaging

2 Configuração Inicial do FCM

3 Implementação para Android

4 Implementação para iOS

5 Backend e Estratégias de Envio

6 Otimização e Boas Práticas


INTRODUÇÃO

Por que Push Notifications Importam em 2026


As notificações push continuam sendo uma das ferramentas mais poderosas para engajamento mobile em 2026. Dados recentes mostram que aplicativos com push notifications implementadas corretamente têm 26% mais retenção de usuários após 90 dias e taxas de abertura médias de 7.8% globalmente.

Firebase Cloud Messaging (FCM) se estabeleceu como a solução líder para notificações cross-platform, processando mais de 300 bilhões de mensagens por dia globalmente. Com suporte nativo para iOS, Android, Web e Flutter, o FCM oferece uma infraestrutura robusta que escala automaticamente.

PONTO-CHAVE

O FCM é gratuito até 100 milhões de mensagens por mês, com latência média de 250ms para entrega global e 99.9% de uptime garantido pelo Google.


Diagrama da arquitetura do Firebase Cloud Messaging mostrando fluxo de mensagens

“Push notifications bem implementadas podem aumentar o engajamento em até 88% e reduzir churn em 65%”

— Estudo Mobile Marketing Association 2026


FUNDAMENTOS

Entendendo o Firebase Cloud Messaging


Arquitetura e Componentes

O FCM funciona como intermediário entre seu servidor de aplicação e os dispositivos dos usuários. A arquitetura consiste em três componentes principais:

Componentes do FCM

FCM SDK — Integrado ao app cliente para receber mensagens e gerenciar tokens

Servidor FCM — Infraestrutura do Google que roteia e entrega mensagens

App Server — Seu backend que envia requisições de mensagem para o FCM


Tipos de Mensagem

O FCM suporta dois tipos principais de mensagens, cada uma com casos de uso específicos:

Notification Messages

✓ Interface automática de notificação

✓ Ideal para alertas simples

✓ Máximo 4KB de payload


Data Messages

• Controle total sobre processamento

• Executadas em background

• Payload customizável até 4KB


PONTO-CHAVE

Data messages têm prioridade mais alta e são ideais para sincronização de dados críticos, enquanto notification messages são otimizadas para conservar bateria.


CONFIGURAÇÃO

Setup Inicial do Projeto Firebase


Criando o Projeto Firebase

1

Console Firebase

Acesse console.firebase.google.com e crie um novo projeto. Em 2026, o processo está mais streamlined com templates específicos para mobile.


2

Configuração de Apps

Adicione seus apps iOS e Android com os bundle identifiers corretos. O FCM agora suporta configuração simultânea para múltiplas plataformas.


3

Ativação do FCM

No painel de Cloud Messaging, ative o serviço e obtenha sua Server Key. Esta será necessária para autenticação do backend.


Configuração de Certificados iOS

Para iOS, você precisa configurar certificados APNs (Apple Push Notification service). Em 2026, o processo foi simplificado com suporte a tokens de autenticação:

EXPLICAÇÃO DO PROCESSO

Vamos configurar o APNs usando tokens de autenticação, que são mais seguros e não expiram como os certificados tradicionais.


// 1. Gerar APNs Auth Key no Apple Developer Portal
// 2. Download do arquivo .p8
// 3. Upload no Firebase Console em Project Settings > Cloud Messaging
// 4. Configurar Key ID, Team ID, e Bundle ID

// Exemplo de configuração programática
{
  "platform": "ios",
  "apns": {
    "headers": {
      "authorization": "bearer {token}",
      "apns-topic": "com.exemplo.app"
    },
    "payload": {
      "aps": {
        "alert": {
          "title": "Título da Notificação",
          "body": "Conteúdo da mensagem"
        },
        "badge": 1,
        "sound": "default"
      }
    }
  }
}

Tela de configuração do console Firebase mostrando setup para iOS e Android

“A configuração correta dos certificados iOS é crucial – 73% dos problemas de entrega de push notifications em iOS são relacionados a certificados mal configurados”

— Firebase Support Team 2026



ANDROID

Implementação Completa para Android


Configuração do Gradle

A implementação Android requer a configuração correta das dependências do Firebase. Em 2026, a versão mais recente do Firebase SDK oferece melhor performance e recursos aprimorados:

EXPLICAÇÃO DO CÓDIGO

Configuração das dependências Firebase no build.gradle do projeto e módulo app.


// build.gradle (Project level)
buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.15'
    }
}

// build.gradle (App level)
plugins {
    id 'com.google.gms.google-services'
}

dependencies {
    implementation platform('com.google.firebase:firebase-bom:32.7.0')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-analytics'
    
    // Para Kotlin Coroutines (recomendado em 2026)
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3'
}

Service de Mensagens FCM

O FirebaseMessagingService é o componente central que processa mensagens recebidas. A implementação moderna em Kotlin oferece melhor performance:

EXPLICAÇÃO DO CÓDIGO

Service personalizado que herda de FirebaseMessagingService para processar mensagens recebidas e gerenciar tokens.


class MyFirebaseMessagingService : FirebaseMessagingService() {
    
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        
        // Log dados da mensagem
        Log.d("FCM", "From: ${remoteMessage.from}")
        
        // Processar data payload
        if (remoteMessage.data.isNotEmpty()) {
            Log.d("FCM", "Message data payload: ${remoteMessage.data}")
            processDataMessage(remoteMessage.data)
        }
        
        // Processar notification payload
        remoteMessage.notification?.let { notification ->
            Log.d("FCM", "Message Notification Body: ${notification.body}")
            showNotification(
                title = notification.title ?: "Notificação",
                body = notification.body ?: "",
                data = remoteMessage.data
            )
        }
    }
    
    override fun onNewToken(token: String) {
        Log.d("FCM", "Refreshed token: $token")
        
        // Enviar token para seu servidor
        sendTokenToServer(token)
    }
    
    private fun processDataMessage(data: Map<String, String>) {
        val type = data["type"]
        val payload = data["payload"]
        
        when (type) {
            "sync_data" -> {
                // Sincronizar dados em background
                GlobalScope.launch {
                    syncUserData(payload)
                }
            }
            "update_ui" -> {
                // Atualizar interface
                updateUI(payload)
            }
            else -> {
                Log.w("FCM", "Unknown message type: $type")
            }
        }
    }
    
    private fun showNotification(title: String, body: String, data: Map<String, String>) {
        val intent = Intent(this, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            // Adicionar dados extras se necessário
            data.forEach { (key, value) ->
                putExtra(key, value)
            }
        }
        
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent, 
            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
        )
        
        val channelId = "fcm_default_channel"
        val notificationBuilder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(body)
            .setAutoCancel(true)
            .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
            .setContentIntent(pendingIntent)
        
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        
        // Criar canal para Android O+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Firebase Cloud Messages",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Canal para notificações Firebase"
            }
            notificationManager.createNotificationChannel(channel)
        }
        
        notificationManager.notify(0, notificationBuilder.build())
    }
}

Configuração do AndroidManifest

EXPLICAÇÃO DO CÓDIGO

Registrar o service no AndroidManifest.xml com as permissões necessárias e configurações de notificação.


<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.VIBRATE" />
    
    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        
        <!-- Service FCM -->
        <service
            android:name=".MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        
        <!-- Metadata para ícone de notificação padrão -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_notification" />
            
        <!-- Cor padrão para notificações -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/colorAccent" />
            
        <!-- Canal padrão para Android O+ -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="fcm_default_channel" />
            
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
    </application>
</manifest>

PONTO-CHAVE

O atributo launchMode="singleTop" na MainActivity evita criação de múltiplas instâncias ao abrir notificações.


Obtenção e Gerenciamento de Tokens

Os tokens FCM são únicos por instalação do app e essenciais para targeting de mensagens. Em 2026, a gestão de tokens tornou-se mais robusta:

EXPLICAÇÃO DO CÓDIGO

Implementação para obter e gerenciar tokens FCM com tratamento de erros e cache local.


class FCMTokenManager(private val context: Context) {
    
    private val prefs = context.getSharedPreferences("fcm_prefs", Context.MODE_PRIVATE)
    private val SERVER_URL = "https://seu-backend.com/api/register-token"
    
    suspend fun getAndSendToken() {
        try {
            val token = FirebaseMessaging.getInstance().token.await()
            Log.d("FCM", "Current token: $token")
            
            val lastSentToken = prefs.getString("last_sent_token", null)
            
            if (token != lastSentToken) {
                sendTokenToServer(token)
                prefs.edit()
                    .putString("last_sent_token", token)
                    .putLong("token_timestamp", System.currentTimeMillis())
                    .apply()
            }
            
        } catch (e: Exception) {
            Log.e("FCM", "Failed to get FCM token", e)
        }
    }
    
    private suspend fun sendTokenToServer(token: String) {
        try {
            val client = OkHttpClient()
            val json = JSONObject().apply {
                put("token", token)
                put("platform", "android")
                put("app_version", BuildConfig.VERSION_NAME)
                put("device_id", getDeviceId())
                put("user_id", getCurrentUserId())
            }
            
            val body = json.toString().toRequestBody("application/json".toMediaType())
            val request = Request.Builder()
                .url(SERVER_URL)
                .post(body)
                .addHeader("Authorization", "Bearer ${getAuthToken()}")
                .build()
            
            val response = client.newCall(request).execute()
            
            if (response.isSuccessful) {
                Log.d("FCM", "Token successfully sent to server")
            } else {
                Log.e("FCM", "Failed to send token: ${response.code}")
            }
            
        } catch (e: Exception) {
            Log.e("FCM", "Error sending token to server", e)
        }
    }
    
    fun subscribeToTopic(topic: String) {
        FirebaseMessaging.getInstance().subscribeToTopic(topic)
            .addOnCompleteListener { task ->
                var msg = "Subscribed to $topic"
                if (!task.isSuccessful) {
                    msg = "Subscribe to $topic failed"
                }
                Log.d("FCM", msg)
            }
    }
    
    fun unsubscribeFromTopic(topic: String) {
        FirebaseMessaging.getInstance().unsubscribeFromTopic(topic)
            .addOnCompleteListener { task ->
                var msg = "Unsubscribed from $topic"
                if (!task.isSuccessful) {
                    msg = "Unsubscribe from $topic failed"
                }
                Log.d("FCM", msg)
            }
    }
    
    private fun getDeviceId(): String {
        return Settings.Secure.getString(
            context.contentResolver,
            Settings.Secure.ANDROID_ID
        )
    }
}

App Android recebendo notificação push com service FCM processando



iOS

Implementação Completa para iOS


Configuração do Podfile

Para iOS, utilizamos CocoaPods para gerenciar as dependências Firebase. A versão 2026 inclui melhorias significativas de performance:

EXPLICAÇÃO DO CÓDIGO

Configuração do Podfile com as dependências Firebase necessárias para push notifications.


# Podfile
platform :ios, '13.0'
use_frameworks!

target 'SeuApp' do
  # Firebase Core (obrigatório)
  pod 'Firebase/Core'
  
  # Firebase Cloud Messaging
  pod 'Firebase/Messaging'
  
  # Firebase Analytics (recomendado)
  pod 'Firebase/Analytics'
  
  # Para SwiftUI (se aplicável)
  pod 'Firebase/Firestore'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
    end
  end
end

AppDelegate Configuration

O AppDelegate é onde configuramos o Firebase e registramos para notificações push. A implementação em Swift é limpa e moderna:

EXPLICAÇÃO DO CÓDIGO

Configuração completa do AppDelegate com Firebase, UNUserNotificationCenter e Messaging delegate.


import UIKit
import Firebase
import FirebaseMessaging
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, 
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // Configurar Firebase
        FirebaseApp.configure()
        
        // Configurar Messaging
        Messaging.messaging().delegate = self
        
        // Configurar UNUserNotificationCenter
        UNUserNotificationCenter.current().delegate = self
        
        // Solicitar permissão para notificações
        requestNotificationPermission()
        
        // Registrar para remote notifications
        application.registerForRemoteNotifications()
        
        return true
    }
    
    private func requestNotificationPermission() {
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions
        ) { granted, error in
            print("Permission granted: \(granted)")
            if let error = error {
                print("Error requesting notification permission: \(error)")
            }
        }
    }
    
    // MARK: - Remote Notifications
    func application(_ application: UIApplication, 
                    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        
        print("APNs token retrieved: \(deviceToken)")
        
        // Definir APNs token no Messaging
        Messaging.messaging().apnsToken = deviceToken
    }
    
    func application(_ application: UIApplication, 
                    didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Unable to register for remote notifications: \(error.localizedDescription)")
    }
    
    // Para processar notificações recebidas em background
    func application(_ application: UIApplication, 
                    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        
        print("Received remote notification: \(userInfo)")
        
        // Processar data payload
        if let data = userInfo["data"] as? [String: Any] {
            processDataPayload(data)
        }
        
        completionHandler(.newData)
    }
    
    private func processDataPayload(_ data: [String: Any]) {
        guard let type = data["type"] as? String else { return }
        
        switch type {
        case "sync_data":
            // Sincronizar dados em background
            performBackgroundSync(data)
        case "update_ui":
            // Agendar atualização de UI para quando app for ativo
            scheduleUIUpdate(data)
        default:
            print("Unknown message type: \(type)")
        }
    }
}

Extensions e Delegates

EXPLICAÇÃO DO CÓDIGO

Extensions para implementar os protocolos MessagingDelegate e UNUserNotificationCenterDelegate.


// MARK: - MessagingDelegate
extension AppDelegate: MessagingDelegate {
    
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let token = fcmToken else { return }
        
        print("Firebase registration token: \(token)")
        
        // Enviar token para servidor
        sendTokenToServer(token)
        
        // Armazenar localmente
        UserDefaults.standard.set(token, forKey: "fcm_token")
        UserDefaults.standard.set(Date(), forKey: "fcm_token_date")
        
        // Postar notificação para outras partes do app
        NotificationCenter.default.post(
            name: NSNotification.Name("FCMTokenRefreshed"),
            object: nil,
            userInfo: ["token": token]
        )
    }
    
    private func sendTokenToServer(_ token: String) {
        guard let url = URL(string: "https://seu-backend.com/api/register-token") else { return }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(getAuthToken())", forHTTPHeaderField: "Authorization")
        
        let payload: [String: Any] = [
            "token": token,
            "platform": "ios",
            "app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
            "device_id": UIDevice.current.identifierForVendor?.uuidString ?? "",
            "user_id": getCurrentUserId()
        ]
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: payload)
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let error = error {
                    print("Error sending token: \(error)")
                    return
                }
                
                if let httpResponse = response as? HTTPURLResponse {
                    print("Token sent with status: \(httpResponse.statusCode)")
                }
            }.resume()
            
        } catch {
            print("Error serializing token payload: \(error)")
        }
    }
}

// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
    
    // Notificação recebida em foreground
    func userNotificationCenter(_ center: UNUserNotificationCenter, 
                              willPresent notification: UNNotification, 
                              withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        let userInfo = notification.request.content.userInfo
        print("Notification received in foreground: \(userInfo)")
        
        // Processar data payload se existir
        if let data = userInfo["data"] as? [String: Any] {
            processDataPayload(data)
        }
        
        // Exibir notificação mesmo em foreground
        completionHandler([.alert, .badge, .sound])
    }
    
    // Usuário tocou na notificação
    func userNotificationCenter(_ center: UNUserNotificationCenter, 
                              didReceive response: UNNotificationResponse, 
                              withCompletionHandler completionHandler: @escaping () -> Void) {
        
        let userInfo = response.notification.request.content.userInfo
        print("User tapped notification: \(userInfo)")
        
        // Navegar para tela específica baseado no payload
        if let deepLink = userInfo["deep_link"] as? String {
            handleDeepLink(deepLink)
        }
        
        // Processar ação específica
        if let action = userInfo["action"] as? String {
            handleNotificationAction(action, userInfo: userInfo)
        }
        
        completionHandler()
    }
    
    private func handleDeepLink(_ deepLink: String) {
        // Implementar navegação baseada no deep link
        guard let url = URL(string: deepLink) else { return }
        
        DispatchQueue.main.async {
            // Navegar para tela correspondente
            // Por exemplo, usando um NavigationManager
            NavigationManager.shared.navigate(to: url)
        }
    }
    
    private func handleNotificationAction(_ action: String, userInfo: [AnyHashable: Any]) {
        switch action {
        case "open_chat":
            // Abrir chat específico
            if let chatId = userInfo["chat_id"] as? String {
                openChat(chatId)
            }
        case "view_order":
            // Visualizar pedido
            if let orderId = userInfo["order_id"] as? String {
                viewOrder(orderId)
            }
        default:
            print("Unknown notification action: \(action)")
        }
    }
}

PONTO-CHAVE

O método willPresent permite exibir notificações mesmo quando o app está em foreground, melhorando a experiência do usuário.


Gerenciamento de Topics

Topics permitem segmentação automática de usuários sem necessidade de gerenciar listas de tokens individualmente:

EXPLICAÇÃO DO CÓDIGO

Classe para gerenciar inscrições em topics de forma organizada e com cache local.


import Firebase
import FirebaseMessaging

class FCMTopicsManager {
    
    static let shared = FCMTopicsManager()
    private let messaging = Messaging.messaging()
    private let userDefaults = UserDefaults.standard
    
    private init() {}
    
    // MARK: - Topic Subscription
    func subscribeToTopic(_ topic: String, completion: @escaping (Bool) -> Void = { _ in }) {
        messaging.subscribe(toTopic: topic) { error in
            if let error = error {
                print("Error subscribing to topic \(topic): \(error)")
                completion(false)
            } else {
                print("Successfully subscribed to topic: \(topic)")
                self.addTopicToLocal(topic)
                completion(true)
            }
        }
    }
    
    func unsubscribeFromTopic(_ topic: String, completion: @escaping (Bool) -> Void = { _ in }) {
        messaging.unsubscribe(fromTopic: topic) { error in
            if let error = error {
                print("Error unsubscribing from topic \(topic): \(error)")
                completion(false)
            } else {
                print("Successfully unsubscribed from topic: \(topic)")
                self.removeTopicFromLocal(topic)
                completion(true)
            }
        }
    }
    
    // MARK: - Bulk Operations
    func subscribeToMultipleTopics(_ topics: [String]) {
        topics.forEach { topic in
            subscribeToTopic(topic)
        }
    }
    
    func unsubscribeFromAllTopics() {
        let subscribedTopics = getSubscribedTopics()
        subscribedTopics.forEach { topic in
            unsubscribeFromTopic(topic)
        }
    }
    
    // MARK: - User-Based Topics
    func subscribeToUserTopics(userId: String, preferences: UserNotificationPreferences) {
        // Topic específico do usuário
        subscribeToTopic("user_\(userId)")
        
        // Topics baseados em preferências
        if preferences.marketing {
            subscribeToTopic("marketing")
        }
        
        if preferences.updates {
            subscribeToTopic("app_updates")
        }
        
        if preferences.social {
            subscribeToTopic("social_notifications")
        }
        
        // Topics baseados em localização
        if let region = preferences.region {
            subscribeToTopic("region_\(region)")
        }
        
        // Topics baseados em interesse
        preferences.interests.forEach { interest in
            subscribeToTopic("interest_\(interest)")
        }
    }
    
    // MARK: - Local Storage
    private func addTopicToLocal(_ topic: String) {
        var topics = getSubscribedTopics()
        if !topics.contains(topic) {
            topics.append(topic)
            userDefaults.set(topics, forKey: "subscribed_topics")
        }
    }
    
    private func removeTopicFromLocal(_ topic: String) {
        var topics = getSubscribedTopics()
        topics.removeAll { $0 == topic }
        userDefaults.set(topics, forKey: "subscribed_topics")
    }
    
    func getSubscribedTopics() -> [String] {
        return userDefaults.stringArray(forKey: "subscribed_topics") ?? []
    }
    
    // MARK: - Conditional Topics
    func updateConditionalTopics(user: User) {
        // Limpar topics antigos se necessário
        let oldTopics = getSubscribedTopics().filter { $0.hasPrefix("level_") }
        oldTopics.forEach { unsubscribeFromTopic($0) }
        
        // Adicionar topic baseado no nível do usuário
        subscribeToTopic("level_\(user.level)")
        
        // Topics baseados em status premium
        if user.isPremium {
            subscribeToTopic("premium_users")
        } else {
            unsubscribeFromTopic("premium_users")
            subscribeToTopic("free_users")
        }
        
        // Topics baseados em atividade recente
        let daysSinceLastLogin = Date().timeIntervalSince(user.lastLogin) / (24 * 60 * 60)
        if daysSinceLastLogin > 7 {
            subscribeToTopic("inactive_users")
        } else {
            unsubscribeFromTopic("inactive_users")
        }
    }
}

struct UserNotificationPreferences {
    let marketing: Bool
    let updates: Bool
    let social: Bool
    let region: String?
    let interests: [String]
}

Diálogo de permissão para notificações iOS e notificação recebida



BACKEND

Estratégias de Backend e Envio


Implementação do Servidor com Node.js

O backend é crucial para gerenciar tokens, segmentar usuários e enviar notificações de forma eficiente. Em 2026, as melhores práticas incluem rate limiting, retry logic e analytics detalhados:

EXPLICAÇÃO DO CÓDIGO

Implementação de um servidor Node.js com Express para gerenciar tokens FCM e enviar notificações segmentadas.


const express = require('express');
const admin = require('firebase-admin');
const mongoose = require('mongoose');
const rateLimit = require('express-rate-limit');
const cors = require('cors');

const app = express();

// Middleware
app.use(express.json({ limit: '10mb' }));
app.use(cors());

// Rate Limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // máximo 100 requests por IP
  message: 'Too many requests from this IP'
});
app.use('/api/', limiter);

// Inicializar Firebase Admin SDK
const serviceAccount = require('./path/to/serviceAccountKey.json');
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

// Schema do MongoDB para tokens
const TokenSchema = new mongoose.Schema({
  userId: { type: String, required: true, index: true },
  token: { type: String, required: true, unique: true },
  platform: { type: String, enum: ['ios', 'android'], required: true },
  appVersion: String,
  deviceId: String,
  isActive: { type: Boolean, default: true },
  createdAt: { type: Date, default: Date.now },
  lastUsed: { type: Date, default: Date.now },
  topics: [String]
});

const Token = mongoose.model('Token', TokenSchema);

// Schema para analytics de notificações
const NotificationLogSchema = new mongoose.Schema({
  messageId: String,
  userId: String,
  title: String,
  body: String,
  platform: String,
  status: { type: String, enum: ['sent', 'delivered', 'failed', 'clicked'] },
  errorCode: String,
  sentAt: { type: Date, default: Date.now },
  deliveredAt: Date,
  clickedAt: Date
});

const NotificationLog = mongoose.model('NotificationLog', NotificationLogSchema);

class FCMService {
  constructor() {
    this.messaging = admin.messaging();
  }

  // Registrar novo token
  async registerToken(req, res) {
    try {
      const { token, platform, appVersion, deviceId, userId } = req.body;

      // Validar token com Firebase
      const isValidToken = await this.validateToken(token);
      if (!isValidToken) {
        return res.status(400).json({ error: 'Invalid FCM token' });
      }

      // Desativar tokens antigos do mesmo dispositivo
      await Token.updateMany(
        { userId, deviceId, platform },
        { isActive: false }
      );

      // Salvar novo token
      const newToken = new Token({
        userId,
        token,
        platform,
        appVersion,
        deviceId,
        isActive: true,
        lastUsed: new Date()
      });

      await newToken.save();

      console.log(`Token registered for user ${userId}: ${token.substring(0, 20)}...`);
      res.json({ success: true, message: 'Token registered successfully' });

    } catch (error) {
      console.error('Error registering token:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

  // Validar token com Firebase
  async validateToken(token) {
    try {
      const message = {
        data: { test: 'true' },
        token: token,
        dryRun: true
      };
      
      await this.messaging.send(message);
      return true;
    } catch (error) {
      console.error('Token validation failed:', error);
      return false;
    }
  }

  // Enviar para usuário específico
  async sendToUser(userId, notification, data = {}) {
    try {
      const tokens = await Token.find({ userId, isActive: true });
      
      if (tokens.length === 0) {
        console.log(`No active tokens for user ${userId}`);
        return { success: false, error: 'No active tokens' };
      }

      const results = await Promise.allSettled(
        tokens.map(tokenDoc => this.sendSingleMessage(tokenDoc, notification, data))
      );

      const successCount = results.filter(r => r.status === 'fulfilled').length;
      const failureCount = results.length - successCount;

      console.log(`Messages sent to user ${userId}: ${successCount} success, ${failureCount} failed`);

      return {
        success: true,
        totalSent: successCount,
        totalFailed: failureCount
      };

    } catch (error) {
      console.error('Error sending to user:', error);
      return { success: false, error: error.message };
    }
  }

  // Enviar mensagem individual
  async sendSingleMessage(tokenDoc, notification, data) {
    try {
      const message = {
        notification: {
          title: notification.title,
          body: notification.body,
          ...(notification.imageUrl && { imageUrl: notification.imageUrl })
        },
        data: {
          ...data,
          clickAction: data.clickAction || 'FLUTTER_NOTIFICATION_CLICK',
          timestamp: Date.now().toString()
        },
        token: tokenDoc.token,
        android: {
          priority: 'high',
          notification: {
            icon: 'ic_notification',
            color: '#667eea',
            sound: 'default',
            channelId: 'fcm_default_channel'
          }
        },
        apns: {
          headers: {
            'apns-priority': '10'
          },
          payload: {
            aps: {
              badge: 1,
              sound: 'default',
              'content-available': 1
            }
          }
        }
      };

      const response = await this.messaging.send(message);
      
      // Log de sucesso
      await this.logNotification({
        messageId: response,
        userId: tokenDoc.userId,
        title: notification.title,
        body: notification.body,
        platform: tokenDoc.platform,
        status: 'sent'
      });

      // Atualizar último uso do token
      tokenDoc.lastUsed = new Date();
      await tokenDoc.save();

      return response;

    } catch (error) {
      console.error('Error sending single message:', error);

      // Log de erro
      await this.logNotification({
        userId: tokenDoc.userId,
        title: notification.title,
        body: notification.body,
        platform: tokenDoc.platform,
        status: 'failed',
        errorCode: error.code
      });

      // Desativar tokens inválidos
      if (error.code === 'messaging/invalid-registration-token' || 
          error.code === 'messaging/registration-token-not-registered') {
        tokenDoc.isActive = false;
        await tokenDoc.save();
      }

      throw error;
    }
  }

  // Enviar para múltiplos usuários (batch)
  async sendBatchNotification(userIds, notification, data = {}) {
    const batchSize = 500; // Firebase recomenda máximo 500 por batch
    const results = [];

    for (let i = 0; i < userIds.length; i += batchSize) {
      const batch = userIds.slice(i, i + batchSize);
      const batchPromises = batch.map(userId => 
        this.sendToUser(userId, notification, data)
      );
      
      const batchResults = await Promise.allSettled(batchPromises);
      results.push(...batchResults);

      // Delay entre batches para evitar rate limiting
      if (i + batchSize < userIds.length) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }

    return results;
  }

  // Enviar para topic
  async sendToTopic(topic, notification, data = {}) {
    try {
      const message = {
        notification,
        data,
        topic: topic,
        android: {
          priority: 'high'
        },
        apns: {
          headers: {
            'apns-priority': '10'
          }
        }
      };

      const response = await this.messaging.send(message);
      console.log(`Message sent to topic ${topic}:`, response);

      return { success: true, messageId: response };

    } catch (error) {
      console.error('Error sending to topic:', error);
      return { success: false, error: error.message };
    }
  }

  // Log de notificações para analytics
  async logNotification(logData) {
    try {
      const log = new NotificationLog(logData);
      await log.save();
    } catch (error) {
      console.error('Error logging notification:', error);
    }
  }
}

const fcmService = new FCMService();

// Routes
app.post('/api/register-token', (req, res) => fcmService.registerToken(req, res));

app.post('/api/send-notification', async (req, res) => {
  try {
    const { userId, title, body, data, imageUrl } = req.body;

    const result = await fcmService.sendToUser(userId, {
      title,
      body,
      imageUrl
    }, data);

    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/send-batch', async (req, res) => {
  try {
    const { userIds, title, body, data } = req.body;

    const results = await fcmService.sendBatchNotification(userIds, {
      title,
      body
    }, data);

    res.json({ results });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/send-topic', async (req, res) => {
  try {
    const { topic, title, body, data } = req.body;

    const result = await fcmService.sendToTopic(topic, {
      title,
      body
    }, data);

    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`FCM Server running on port ${PORT}`);
});

PONTO-CHAVE

O sistema de rate limiting e batch processing é essencial para evitar throttling do Firebase. Recomenda-se máximo 500 mensagens por batch com delay de 1s entre lotes.


Segmentação Avançada

A segmentação eficaz é fundamental para campanhas de push notification bem-sucedidas. Dados de 2026 mostram que mensagens segmentadas têm 76% mais taxa de abertura:

Dashboard mostrando analytics de notificações push com dados de segmentação

ESTRATÉGIA 01

Segmentação Comportamental

Baseada em ações do usuário como tempo no app, features utilizadas, e padrões de uso.

IMPLEMENTAÇÃO — Algoritmo de scoring comportamental

async function calculateUserSegment(userId) {
  const user = await User.findById(userId);
  const analytics = await UserAnalytics.findOne({ userId });
  
  let score = 0;
  
  // Frequência de uso (40% do score)
  const daysActive = analytics.activeDaysLast30;
  score += (daysActive / 30) * 40;
  
  // Engajamento (35% do score)
  const avgSessionTime = analytics.avgSessionTime;
  score += Math.min(avgSessionTime / 600, 1) * 35;
  
  // Funcionalidades usadas (25% do score)
  const featuresUsed = analytics.featuresUsed.length;
  score += (featuresUsed / 10) * 25;
  
  if (score >= 80) return 'power_user';
  if (score >= 50) return 'active_user';
  if (score >= 20) return 'casual_user';
  return 'inactive_user';
}

ESTRATÉGIA 02

Timing Otimizado

Envio baseado no fuso horário e horários de maior engajamento do usuário.

IMPLEMENTAÇÃO — Sistema de timing inteligente

async function scheduleNotificationOptimal(userId, notification) {
  const user = await User.findById(userId);
  const timezone = user.timezone || 'UTC';
  
  // Obter estatísticas de engajamento do usuário
  const stats = await getEngagementStats(userId);
  const optimalHour = stats.mostActiveHour || 19; // Default 19h
  
  // Calcular próximo horário ótimo
  const now = moment().tz(timezone);
  let targetTime = moment().tz(timezone).hour(optimalHour).minute(0).second(0);
  
  // Se já passou do horário hoje, agendar para amanhã
  if (targetTime.isBefore(now)) {
    targetTime.add(1, 'day');
  }
  
  // Evitar finais de semana para notificações de trabalho
  if (notification.category === 'work' && targetTime.day() === 0 || targetTime.day() === 6) {
    targetTime.add(targetTime.day() === 0 ? 1 : 2, 'days');
  }
  
  // Agendar notificação
  const delay = targetTime.diff(now);
  
  setTimeout(async () => {
    await fcmService.sendToUser(userId, notification);
  }, delay);
  
  console.log(`Notification scheduled for ${targetTime.format()} (${timezone})`);
}

“Notificações enviadas no horário ótimo do usuário têm 3.2x mais engajamento comparado a envios em horários aleatórios”

— Mobile Engagement Report 2026



OTIMIZAÇÃO

Boas Práticas e Otimização de Performance


Estratégias de Retenção

Em 2026, as push notifications evoluíram além de simples alertas. Estratégias baseadas em dados comportamentais mostram resultados superiores:

91%

Taxa de entrega média

FCM Global Performance 2026


Melhores Práticas de UX

✓ Personalização baseada no histórico do usuário

✓ Frequência adaptativa (menos para usuários ativos)

✓ Deep links contextuais para ações específicas

✓ A/B testing contínuo de título e conteúdo

✓ Opt-out granular por categoria de notificação


Analytics e Monitoramento

Um sistema robusto de analytics é essencial para otimizar campanhas. Métricas-chave incluem taxa de entrega, abertura, conversão e churn rate:

EXPLICAÇÃO DO CÓDIGO

Sistema de analytics completo para tracking de performance de push notifications.


class NotificationAnalytics {
  
  constructor() {
    this.metrics = {
      sent: 0,
      delivered: 0,
      opened: 0,
      clicked: 0,
      converted: 0,
      unsubscribed: 0
    };
  }
  
  // Gerar relatório de campanha
  async generateCampaignReport(campaignId, dateRange) {
    const pipeline = [
      {
        $match: {
          campaignId: campaignId,
          sentAt: {
            $gte: dateRange.start,
            $lte: dateRange.end
          }
        }
      },
      {
        $group: {
          _id: "$status",
          count: { $sum: 1 },
          avgDeliveryTime: {
            $avg: {
              $subtract: ["$deliveredAt", "$sentAt"]
            }
          }
        }
      }
    ];
    
    const results = await NotificationLog.aggregate(pipeline);
    
    const report = {
      campaignId,
      dateRange,
      metrics: {},
      performance: {}
    };
    
    results.forEach(result => {
      report.metrics[result._id] = result.count;
    });
    
    // Calcular taxas
    const totalSent = report.metrics.sent || 0;
    report.performance = {
      deliveryRate: ((report.metrics.delivered || 0) / totalSent * 100).toFixed(2),
      openRate: ((report.metrics.opened || 0) / totalSent * 100).toFixed(2),
      clickRate: ((report.metrics.clicked || 0) / totalSent * 100).toFixed(2),
      conversionRate: ((report.metrics.converted || 0) / totalSent * 100).toFixed(2),
      unsubscribeRate: ((report.metrics.unsubscribed || 0) / totalSent * 100).toFixed(2)
    };
    
    return report;
  }
  
  // Análise de cohort
  async analyzeCohort(userCohort, timeframe) {
    const cohortAnalysis = {
      totalUsers: userCohort.length,
      retentionByDay: {},
      engagementTrends: {}
    };
    
    for (let day = 1; day <= timeframe; day++) {
      const startDate = new Date();
      startDate.setDate(startDate.getDate() - day);
      
      const activeUsers = await this.getActiveUsers(userCohort, startDate);
      const engagedUsers = await this.getEngagedUsers(userCohort, startDate);
      
      cohortAnalysis.retentionByDay[day] = {
        activeUsers: activeUsers.length,
        retentionRate: (activeUsers.length / cohortAnalysis.totalUsers * 100).toFixed(2),
        engagementRate: (engagedUsers.length / activeUsers.length * 100).toFixed(2)
      };
    }
    
    return cohortAnalysis;
  }
  
  // Otimização de timing
  async optimizeNotificationTiming(userId) {
    const userLogs = await NotificationLog.find({
      userId: userId,
      status: { $in: ['opened', 'clicked'] },
      sentAt: {
        $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Últimos 30 dias
      }
    });
    
    // Analisar padrões por hora do dia
    const hourlyEngagement = {};
    
    userLogs.forEach(log => {
      const hour = new Date(log.sentAt).getHours();
      if (!hourlyEngagement[hour]) {
        hourlyEngagement[hour] = { total: 0, engaged: 0 };
      }
      
      hourlyEngagement[hour].total++;
      if (log.status === 'clicked') {
        hourlyEngagement[hour].engaged++;
      }
    });
    
    // Encontrar horário com melhor engajamento
    let bestHour = 19; // Default
    let bestRate = 0;
    
    Object.entries(hourlyEngagement).forEach(([hour, data]) => {
      const rate = data.engaged / data.total;
      if (rate > bestRate && data.total >= 5) { // Mínimo 5 notificações para ser significativo
        bestRate = rate;
        bestHour = parseInt(hour);
      }
    });
    
    return {
      optimalHour: bestHour,
      engagementRate: (bestRate * 100).toFixed(2),
      dataPoints: Object.keys(hourlyEngagement).length
    };
  }
  
  // Detectar fadiga de notificação
  async detectNotificationFatigue(userId) {
    const recentLogs = await NotificationLog.find({
      userId: userId,
      sentAt: {
        $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Últimos 7 dias
      }
    }).sort({ sentAt: -1 });
    
    if (recentLogs.length < 5) {
      return { fatigueLevel: 'low', recommendation: 'normal' };
    }
    
    // Calcular tendência de engajamento
    const engagementTrend = [];
    for (let i = 0; i < Math.min(recentLogs.length, 10); i++) {
      const log = recentLogs[i];
      engagementTrend.push(log.status === 'clicked' ? 1 : 0);
    }
    
    const recentEngagement = engagementTrend.slice(0, 5).reduce((a, b) => a + b, 0) / 5;
    const olderEngagement = engagementTrend.slice(5, 10).reduce((a, b) => a + b, 0) / 5;
    
    const trend = recentEngagement - olderEngagement;
    
    if (trend < -0.3) {
      return { fatigueLevel: 'high', recommendation: 'reduce_frequency' };
    } else if (trend < -0.1) {
      return { fatigueLevel: 'medium', recommendation: 'personalize_more' };
    } else {
      return { fatigueLevel: 'low', recommendation: 'normal' };
    }
  }
  
  // Métricas em tempo real
  async getRealTimeMetrics(timeWindow = 60) { // minutos
    const since = new Date(Date.now() - timeWindow * 60 * 1000);
    
    const pipeline = [
      {
        $match: {
          sentAt: { $gte: since }
        }
      },
      {
        $group: {
          _id: {
            status: "$status",
            minute: {
              $dateToString: {
                format: "%Y-%m-%d %H:%M",
                date: "$sentAt"
              }
            }
          },
          count: { $sum: 1 }
        }
      },
      {
        $sort: { "_id.minute": -1 }
      }
    ];
    
    const results = await NotificationLog.aggregate(pipeline);
    
    // Formatar para dashboard
    const metrics = {
      timeline: {},
      totals: { sent: 0, delivered: 0, opened: 0, clicked: 0 }
    };
    
    results.forEach(result => {
      const minute = result._id.minute;
      const status = result._id.status;
      
      if (!metrics.timeline[minute]) {
        metrics.timeline[minute] = {};
      }
      
      metrics.timeline[minute][status] = result.count;
      metrics.totals[status] = (metrics.totals[status] || 0) + result.count;
    });
    
    return metrics;
  }
}

Checklist de Otimização

☑ Implementar rate limiting no backend (100 req/min por IP)

☑ Configurar retry logic para tokens inválidos

☑ Usar batch sending para campanias grandes (máx 500/batch)

☑ Implementar cache de tokens com TTL de 24h

☑ Configurar monitoring de entrega em tempo real

☐ Adicionar fallback para APNs em caso de falha FCM

☐ Implementar A/B testing automatizado


AVISO

Evite enviar mais de 6 notificações por dia por usuário. Dados de 2026 mostram que usuários que recebem mais de 6 notificações diárias têm 85% de chance de desativar as notificações permanentemente.



Implementação Completa e Eficiente!

Com este guia completo, você tem todas as ferramentas necessárias para implementar um sistema robusto de push notifications usando Firebase Cloud Messaging. Das configurações básicas até estratégias avançadas de segmentação e analytics, sua aplicação está pronta para engajar usuários de forma inteligente e eficaz.

Dúvidas sobre implementação? Deixe um comentário!