DEV Community

Vaclav Svara
Vaclav Svara

Posted on

Firebase Push Notifications in Capacitor Angular Apps: The Complete Implementation Guide

Published: February 16, 2026
Author: Václav Švára
Generated with assistance from: Claude (Anthropic AI)


Table of Contents


Introduction

Push notifications are essential for modern mobile applications, enabling real-time communication with users even when the app isn't running. However, implementing Firebase Cloud Messaging (FCM) in a Capacitor Angular application involves navigating platform-specific requirements, native code integration, and several potential pitfalls.

This guide provides a complete, production-ready implementation of Firebase push notifications for Capacitor Angular apps, covering both iOS and Android platforms.

What You'll Learn

  • Complete Firebase project setup for mobile apps
  • iOS APNs certificate configuration (the right way)
  • Native iOS implementation with Swift
  • Android Firebase integration
  • Angular service for cross-platform notifications
  • Backend integration for sending notifications
  • Testing strategies and troubleshooting

Architecture Overview

High-Level Flow

┌─────────────────────────────────────────────────────────┐
│                    Your Backend API                     │
│  - Stores FCM tokens in database                        │
│  - Sends notifications via Firebase Admin SDK           │
└────────────────────┬────────────────────────────────────┘
                     │
                     ↓
┌─────────────────────────────────────────────────────────┐
│           Firebase Cloud Messaging (FCM)                │
│  - Routes notifications to correct platform             │
│  - Handles delivery and retries                         │
└────────────┬────────────────────────────┬───────────────┘
             │                            │
             ↓                            ↓
    ┌────────────────┐           ┌───────────────┐
    │      iOS       │           │    Android    │
    │  (via APNs)    │           │  (via FCM)    │
    └────────┬───────┘           └───────┬───────┘
             │                           │
             ↓                           ↓
    ┌────────────────┐           ┌───────────────┐
    │  Capacitor     │           │  Capacitor    │
    │  Plugin        │           │  Plugin       │
    └────────┬───────┘           └───────┬───────┘
             │                           │
             └───────────┬───────────────┘
                         ↓
                ┌─────────────────┐
                │ Angular Service │
                │  - Token mgmt   │
                │  - Listeners    │
                └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Token Flow

  1. App Launch: Angular service initializes notification system
  2. Permission Request: User grants notification permission
  3. Native Token Generation:
    • iOS: APNs generates device token
    • Android: FCM generates token directly
  4. FCM Token Creation: Firebase SDK converts platform tokens to FCM tokens
  5. Token Storage: Angular service receives FCM token
  6. Backend Registration: Token sent to your backend API
  7. Notification Sending: Backend sends notification via Firebase Admin SDK
  8. Platform Routing: FCM routes to APNs (iOS) or FCM (Android)
  9. Delivery: Notification appears on device

Prerequisites

Required Accounts

  • Firebase Account: Free tier available at firebase.google.com
  • Apple Developer Account: $99/year (for iOS push notifications)
  • Google Developer Account: One-time $25 fee (for Android deployment)

Development Environment

# Node.js and npm
node --version  # v18+ recommended
npm --version

# Angular CLI
npm install -g @angular/cli

# Capacitor CLI
npm install -g @capacitor/cli

# Physical devices for testing
# - iOS Simulator does NOT support push notifications
# - Android Emulator with Google Play Services (API 24+)
Enter fullscreen mode Exit fullscreen mode

Required Packages

# Firebase Messaging plugin for Capacitor
npm install @capacitor-firebase/messaging

# Capacitor platforms
npm install @capacitor/ios @capacitor/android
Enter fullscreen mode Exit fullscreen mode

Firebase Project Setup

Step 1: Create Firebase Project

  1. Go to Firebase Console
  2. Click "Add project"
  3. Enter project details:
   Project name: YourApp
   Enable Google Analytics: Yes (recommended)
Enter fullscreen mode Exit fullscreen mode
  1. Click "Create project"

Step 2: Add iOS App

  1. In Firebase Console → Project OverviewAdd appiOS
  2. Enter configuration:
   iOS bundle ID: com.yourcompany.yourapp
   App nickname: YourApp iOS (optional)
   App Store ID: Leave empty
Enter fullscreen mode Exit fullscreen mode
  1. Download GoogleService-Info.plist
  2. Save file for later - you'll add it to Xcode project

Step 3: Add Android App

  1. In Firebase Console → Project OverviewAdd appAndroid
  2. Enter configuration:
   Android package name: com.yourcompany.yourapp
   App nickname: YourApp Android (optional)
   SHA-1: Optional for testing
Enter fullscreen mode Exit fullscreen mode
  1. Download google-services.json
  2. Save file for later - you'll add it to Android project

Step 4: Enable Cloud Messaging

  1. Firebase Console → Project SettingsCloud Messaging
  2. Note the Server Key (for backend integration)
  3. We'll configure APNs in the next section

iOS Configuration

The APNs Certificate vs Authentication Key Dilemma

CRITICAL DISCOVERY: Firebase Cloud Messaging campaigns have different behavior depending on authentication method:

Method Firebase Console Campaigns Direct API Calls Expiration
APNs Certificates (.p12) Works perfectly ✅ Works 1 year
APNs Authentication Keys (.p8) Fails silently ✅ Works Never

⚠️ CRITICAL: Use APNs Certificates, NOT Authentication Keys!

As of February 2026, APNs Authentication Keys (.p8) do NOT work with Firebase Console test messages and campaigns. They fail silently with "0 sends" despite correct configuration.

The Problem: This is a known Firebase SDK issue (GitHub Issue #4042) that has not been resolved. Firebase Console campaigns require APNs Certificates (.p12).

The Solution: Always use APNs Certificates (.p12) for reliable delivery across all scenarios.

Important Notes:

  • APNs Certificates expire after 1 year and must be renewed
  • APNs Authentication Keys never expire but don't work with Firebase Console
  • Direct FCM API calls work with both, but Firebase Console campaigns require Certificates

Creating APNs Certificates

Step 1: Register App ID in Apple Developer

  1. Go to Apple Developer Console
  2. Click "+""App IDs""App"
  3. Configure:
   Description: YourApp
   Bundle ID: com.yourcompany.yourapp (Explicit)
   Capabilities: Enable "Push Notifications"
Enter fullscreen mode Exit fullscreen mode
  1. Click "Register"

Step 2: Generate Certificate Signing Request (CSR)

On your Mac:

  1. Open Keychain Access (Applications → Utilities)
  2. Menu: Keychain AccessCertificate AssistantRequest a Certificate From a Certificate Authority...
  3. Fill in:
   User Email Address: your.email@example.com
   Common Name: YourApp Push Certificate
   CA Email Address: Leave empty
   Request is: Saved to disk
Enter fullscreen mode Exit fullscreen mode
  1. Check "Let me specify key pair information"
  2. Click "Continue"
  3. Save as: YourAppPush.certSigningRequest
  4. Key Pair Information:
   Key Size: 2048 bits
   Algorithm: RSA
Enter fullscreen mode Exit fullscreen mode
  1. Click "Continue""Done"

Step 3: Create APNs Certificate

  1. Go to Apple Developer Console
  2. Click "+" to create new certificate
  3. Select "Apple Push Notification service SSL (Sandbox & Production)"
  4. Click "Continue"
  5. Select your App ID: com.yourcompany.yourapp
  6. Click "Continue"
  7. Upload the CSR file: YourAppPush.certSigningRequest
  8. Click "Continue"
  9. Download the certificate: aps.cer

Step 4: Install Certificate to Keychain

  1. Double-click aps.cer file
  2. Certificate automatically installs to Keychain Access
  3. Open Keychain Accesslogin keychain → My Certificates
  4. Find "Apple Push Services: com.yourcompany.yourapp"
  5. Expand it (click triangle) → verify private key is underneath

Step 5: Export as .p12 File

  1. In Keychain Access, select the certificate (top level, not private key)
  2. Right-click"Export..."
  3. Configure export:
   File Format: Personal Information Exchange (.p12)
   Save As: YourAppPushCert.p12
Enter fullscreen mode Exit fullscreen mode
  1. Enter a password (remember this! e.g., YourApp2026)
  2. Click "Save"
  3. Enter your Mac password to allow export

You now have YourAppPushCert.p12 ready for Firebase.

Step 6: Upload to Firebase Console

  1. Firebase Console → ⚙️ SettingsProject settingsCloud Messaging
  2. Scroll to "Apple app configuration"
  3. Expand "APNs Certificates" section (NOT Authentication Key!)
  4. Click "Upload" for Production certificate:
   Certificate file: YourAppPushCert.p12
   Password: YourApp2026 (your password)
Enter fullscreen mode Exit fullscreen mode
  1. Click "Upload"

Optional: Upload same .p12 for Development certificate if needed

Step 7: Remove APNs Authentication Keys (If Present)

CRITICAL: If you have APNs Authentication Keys configured, Firebase will ignore certificates!

  1. In same "Apple app configuration" section
  2. Find "APNs Authentication Key" section
  3. Click "Delete" for ALL keys (Development and Production)
  4. Confirm deletion

Firebase will now use APNs Certificates exclusively.


iOS Native Implementation

Step 1: Add GoogleService-Info.plist to Xcode

cd your-app
npx cap open ios
Enter fullscreen mode Exit fullscreen mode

In Xcode:

  1. Right-click App folder (under App.xcodeproj)
  2. Select "Add Files to 'App'..."
  3. Select GoogleService-Info.plist
  4. Check "Copy items if needed"
  5. Target: Check "App"
  6. Click "Add"

Verify: File should be in App/App/ directory

Step 2: Install Firebase Pods

Edit ios/App/Podfile:

platform :ios, '13.0'
use_frameworks!

target 'App' do
  capacitor_pods

  # Firebase dependencies
  pod 'Firebase/Messaging'
  pod 'Firebase/Core'
end
Enter fullscreen mode Exit fullscreen mode

Install pods:

cd ios/App
pod install
Enter fullscreen mode Exit fullscreen mode

IMPORTANT: Always open .xcworkspace file after installing pods:

open App.xcworkspace
Enter fullscreen mode Exit fullscreen mode

Step 3: Enable Capabilities in Xcode

  1. Select App target
  2. Go to "Signing & Capabilities" tab
  3. Click "+ Capability"
  4. Add "Push Notifications"
  5. Add "Background Modes"
    • Check "Remote notifications"

Step 4: Configure Info.plist

Add to ios/App/App/Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>
Enter fullscreen mode Exit fullscreen mode

Step 5: Implement AppDelegate.swift

Replace ios/App/App/AppDelegate.swift with:

import UIKit
import Capacitor
import FirebaseCore
import FirebaseMessaging
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Initialize Firebase
        FirebaseApp.configure()

        // Set notification delegate
        UNUserNotificationCenter.current().delegate = self

        // Set messaging delegate
        Messaging.messaging().delegate = self

        // Request notification permissions
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { granted, error in
                if granted {
                    print("✅ Notification permission granted")
                } else {
                    print("❌ Notification permission denied: \(String(describing: error))")
                }
            }
        )

        // Register for remote notifications
        application.registerForRemoteNotifications()

        return true
    }

    // MARK: - Push Notification Registration

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // Convert to hex for logging
        let tokenHex = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("=================================")
        print("✅ APNs Device Token (HEX):")
        print(tokenHex)
        print("=================================")

        // Set APNs token to Firebase
        Messaging.messaging().apnsToken = deviceToken
        print("✅ APNs token set to Firebase Messaging")

        // Explicitly fetch FCM token
        Messaging.messaging().token { token, error in
            if let error = error {
                print("❌ Error fetching FCM token: \(error)")
            } else if let token = token {
                print("=================================")
                print("✅ FCM TOKEN:")
                print(token)
                print("=================================")
                print("Use this token for testing")
            }
        }

        // Notify Capacitor
        NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("❌ Failed to register for remote notifications: \(error)")
        NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
    }

    // MARK: - UNUserNotificationCenterDelegate

    // Foreground notification handling
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        print("📬 Notification received (foreground): \(userInfo)")

        // Show notification even in foreground
        completionHandler([[.banner, .badge, .sound]])
    }

    // Notification tap handling
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        print("📬 Notification tapped: \(userInfo)")

        // TODO: Handle navigation based on notification data
        // Example: Extract deep link and navigate

        completionHandler()
    }

    // MARK: - MessagingDelegate

    // FCM token refresh
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("=================================")
        print("✅ FCM Token Refreshed:")
        print(fcmToken ?? "nil")
        print("=================================")

        // Notify Angular app
        let dataDict: [String: String] = ["token": fcmToken ?? ""]
        NotificationCenter.default.post(
            name: Notification.Name("FCMToken"),
            object: nil,
            userInfo: dataDict
        )
    }

    // MARK: - URL Handling (Keep existing deep link code if you have MSAL)

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        print("🔗 App opened with URL: \(url.absoluteString)")

        // Handle deep links (MSAL, etc.)
        return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
    }

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Firebase initialized on app launch
  • APNs token automatically set to Firebase SDK
  • FCM token explicitly fetched and logged
  • Notifications shown even when app is in foreground
  • Token refresh automatically handled

Android Configuration

Android setup is simpler than iOS - no certificates required!

Step 1: Add google-services.json

Place google-services.json in:

your-app/android/app/google-services.json
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure build.gradle Files

Project-level android/build.gradle:

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.2.1'
        classpath 'com.google.gms:google-services:4.4.0'  // ADD THIS
    }
}
Enter fullscreen mode Exit fullscreen mode

App-level android/app/build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'  // ADD THIS

android {
    // ... existing config
}

dependencies {
    // ... existing dependencies
    implementation platform('com.google.firebase:firebase-bom:32.7.0')
    implementation 'com.google.firebase:firebase-messaging'
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Sync Capacitor

npx cap sync android
Enter fullscreen mode Exit fullscreen mode

That's it for Android! Firebase handles everything else automatically.


Angular Service Implementation

Create a universal push notification service that works on both platforms.

File: src/app/services/push-notification.service.ts

import { Injectable } from '@angular/core';
import { FirebaseMessaging } from '@capacitor-firebase/messaging';
import { Capacitor } from '@capacitor/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PushNotificationService {
  private isNative = Capacitor.isNativePlatform();

  constructor(private http: HttpClient) {}

  /**
   * Initialize push notifications
   * Call this on app startup (after user is authenticated)
   */
  async initialize(): Promise<void> {
    console.log('[PushNotificationService] Initializing...');

    // Only on native platforms
    if (!this.isNative) {
      console.log('[PushNotificationService] Web platform - push notifications not available');
      return;
    }

    console.log('[PushNotificationService] Platform:', Capacitor.getPlatform());

    try {
      // Request permissions
      const permissionResult = await FirebaseMessaging.requestPermissions();
      console.log('[PushNotificationService] Permission result:', permissionResult);

      if (permissionResult.receive !== 'granted') {
        console.warn('[PushNotificationService] Permission denied');
        return;
      }

      // Get FCM token
      const { token } = await FirebaseMessaging.getToken();
      console.log('=================================');
      console.log('[PushNotificationService] FCM Token:', token);
      console.log('=================================');

      // Send token to backend
      await this.registerTokenWithBackend(token);

      // Setup listeners
      this.setupListeners();

      console.log('[PushNotificationService] Initialization complete');
    } catch (error) {
      console.error('[PushNotificationService] Initialization error:', error);
    }
  }

  /**
   * Setup notification event listeners
   */
  private setupListeners(): void {
    // Token refresh listener
    FirebaseMessaging.addListener('tokenReceived', async (event) => {
      console.log('[PushNotificationService] Token refreshed:', event.token);
      await this.registerTokenWithBackend(event.token);
    });

    // Foreground notification listener
    FirebaseMessaging.addListener('notificationReceived', (event) => {
      console.log('[PushNotificationService] Notification received (foreground):', event.notification);

      // TODO: Show in-app notification
      // Example: Display toast, update badge, etc.
      this.handleForegroundNotification(event.notification);
    });

    // Notification tap listener
    FirebaseMessaging.addListener('notificationActionPerformed', (event) => {
      console.log('[PushNotificationService] Notification tapped:', event);

      // TODO: Handle navigation
      // Example: Navigate to specific screen based on notification data
      this.handleNotificationTap(event);
    });
  }

  /**
   * Register FCM token with your backend
   */
  private async registerTokenWithBackend(token: string): Promise<void> {
    try {
      const platform = Capacitor.getPlatform(); // 'ios' or 'android'

      await firstValueFrom(
        this.http.post('/api/push-tokens', {
          token,
          platform
        })
      );

      console.log('[PushNotificationService] Token registered with backend');
    } catch (error) {
      console.error('[PushNotificationService] Failed to register token:', error);
    }
  }

  /**
   * Handle foreground notification
   */
  private handleForegroundNotification(notification: any): void {
    console.log('[PushNotificationService] Handling foreground notification');

    // Example: Show toast notification
    // You can use a toast/notification library here
    const title = notification.title || 'New Notification';
    const body = notification.body || '';

    // TODO: Display in-app notification
    console.log(`Notification: ${title} - ${body}`);
  }

  /**
   * Handle notification tap (user tapped notification)
   */
  private handleNotificationTap(event: any): void {
    console.log('[PushNotificationService] Handling notification tap');

    const data = event.notification?.data;

    if (data) {
      // Example: Navigate based on notification data
      if (data.screen) {
        console.log(`Navigate to: ${data.screen}`);
        // TODO: Use Angular Router to navigate
        // this.router.navigate([data.screen]);
      }

      if (data.itemId) {
        console.log(`Open item: ${data.itemId}`);
        // TODO: Navigate to item detail
        // this.router.navigate(['/items', data.itemId]);
      }
    }
  }

  /**
   * Get delivered notifications (notifications in notification center)
   */
  async getDeliveredNotifications(): Promise<any[]> {
    try {
      const result = await FirebaseMessaging.getDeliveredNotifications();
      console.log('[PushNotificationService] Delivered notifications:', result.notifications);
      return result.notifications;
    } catch (error) {
      console.error('[PushNotificationService] Error getting delivered notifications:', error);
      return [];
    }
  }

  /**
   * Remove delivered notifications by IDs
   */
  async removeDeliveredNotifications(ids: string[]): Promise<void> {
    try {
      await FirebaseMessaging.removeDeliveredNotifications({
        notifications: ids.map(id => ({ id }))
      });
      console.log('[PushNotificationService] Removed notifications:', ids);
    } catch (error) {
      console.error('[PushNotificationService] Error removing notifications:', error);
    }
  }

  /**
   * Remove all delivered notifications
   */
  async removeAllDeliveredNotifications(): Promise<void> {
    try {
      await FirebaseMessaging.removeAllDeliveredNotifications();
      console.log('[PushNotificationService] All notifications removed');
    } catch (error) {
      console.error('[PushNotificationService] Error removing all notifications:', error);
    }
  }

  /**
   * Cleanup listeners (call on app destroy if needed)
   */
  async cleanup(): Promise<void> {
    await FirebaseMessaging.removeAllListeners();
    console.log('[PushNotificationService] Listeners removed');
  }
}
Enter fullscreen mode Exit fullscreen mode

Initialize Service in App Component

File: src/app/app.component.ts

import { Component, OnInit, inject } from '@angular/core';
import { PushNotificationService } from './services/push-notification.service';
import { AuthService } from './services/auth.service'; // Your auth service

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  private pushService = inject(PushNotificationService);
  private authService = inject(AuthService);

  ngOnInit(): void {
    // Wait for user authentication before initializing push
    this.authService.isAuthenticated$.subscribe(async (isAuthenticated) => {
      if (isAuthenticated) {
        console.log('[App] User authenticated, initializing push notifications');
        await this.pushService.initialize();
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Why wait for authentication?

  • Associate FCM token with specific user
  • Avoid requesting permissions before user sees value
  • Better UX - permission request at appropriate time

Backend Integration

Database Schema

Create table to store FCM tokens:

CREATE TABLE push_tokens (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token VARCHAR(255) NOT NULL UNIQUE,
    platform VARCHAR(10) NOT NULL CHECK (platform IN ('ios', 'android')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

    -- Ensure one token per user per platform
    UNIQUE (user_id, platform)
);

CREATE INDEX idx_push_tokens_user_id ON push_tokens(user_id);
CREATE INDEX idx_push_tokens_token ON push_tokens(token);
Enter fullscreen mode Exit fullscreen mode

API Endpoint to Save Token

C# (.NET) Example:

[HttpPost("api/push-tokens")]
[Authorize]
public async Task<IActionResult> RegisterPushToken([FromBody] RegisterPushTokenRequest request)
{
    var userId = User.GetUserId();

    await _pushTokenService.UpsertTokenAsync(userId, request.Token, request.Platform);

    return Ok(new { message = "Token registered successfully" });
}

public class RegisterPushTokenRequest
{
    public required string Token { get; set; }
    public required string Platform { get; set; } // 'ios' or 'android'
}
Enter fullscreen mode Exit fullscreen mode

Install Firebase Admin SDK

For .NET Backend:

dotnet add package FirebaseAdmin
Enter fullscreen mode Exit fullscreen mode

For Node.js Backend:

npm install firebase-admin
Enter fullscreen mode Exit fullscreen mode

Initialize Firebase Admin SDK

C# Example:

using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using Google.Apis.Auth.OAuth2;

public class FirebaseService
{
    private readonly FirebaseApp _firebaseApp;

    public FirebaseService(IConfiguration configuration)
    {
        var credentialPath = configuration["Firebase:ServiceAccountKeyPath"];

        _firebaseApp = FirebaseApp.Create(new AppOptions
        {
            Credential = GoogleCredential.FromFile(credentialPath)
        });
    }

    public async Task<string> SendNotificationAsync(
        string fcmToken,
        string title,
        string body,
        Dictionary<string, string>? data = null)
    {
        var message = new Message
        {
            Token = fcmToken,
            Notification = new Notification
            {
                Title = title,
                Body = body
            },
            Data = data,
            // Platform-specific configuration
            Android = new AndroidConfig
            {
                Priority = Priority.High,
                Notification = new AndroidNotification
                {
                    Sound = "default",
                    ChannelId = "default"
                }
            },
            Apns = new ApnsConfig
            {
                Aps = new Aps
                {
                    Sound = "default",
                    Badge = 1,
                    ContentAvailable = true
                }
            }
        };

        var response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
        Console.WriteLine($"✅ Notification sent: {response}");
        return response;
    }

    public async Task<bool> SendNotificationToUserAsync(
        Guid userId,
        string title,
        string body,
        Dictionary<string, string>? data = null)
    {
        // Get user's tokens from database
        var tokens = await _pushTokenRepository.GetTokensByUserIdAsync(userId);

        if (!tokens.Any())
        {
            Console.WriteLine($"⚠️ User {userId} has no push tokens");
            return false;
        }

        var tasks = tokens.Select(token =>
            SendNotificationAsync(token.Token, title, body, data)
        );

        await Task.WhenAll(tasks);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Example Usage

// Send notification when a new document is processed
await _firebaseService.SendNotificationToUserAsync(
    userId: user.Id,
    title: "Document Processed",
    body: "Your invoice has been successfully processed",
    data: new Dictionary<string, string>
    {
        { "screen", "/documents" },
        { "documentId", document.Id.ToString() },
        { "action", "view_document" }
    }
);
Enter fullscreen mode Exit fullscreen mode

Testing Push Notifications

Test from Firebase Console

  1. Firebase Console → EngageMessaging
  2. Click "Create your first campaign""Firebase Notification messages"
  3. Fill in notification:
   Title: Test Notification
   Text: This is a test push notification
Enter fullscreen mode Exit fullscreen mode
  1. Click "Send test message"
  2. Paste FCM token (from Xcode/Android Studio logs or Angular service)
  3. Click "Test"

Expected Result: Notification appears on device

Test Scenarios

1. Foreground Notification (App Open)

  • App is running and visible
  • Notification should trigger notificationReceived listener
  • iOS: Banner shows by default (due to completionHandler in AppDelegate)
  • Android: Banner shows automatically

2. Background Notification (App in Background)

  • App is running but not visible (Home button pressed)
  • Notification appears in system tray
  • Tapping notification triggers notificationActionPerformed listener
  • App opens and navigates (if implemented)

3. Killed App Notification (App Closed)

  • App is completely closed (swiped away)
  • Notification still appears in system tray
  • Tapping notification opens app and triggers listener

Debugging Tips

iOS - Xcode Console:

Look for these logs:
✅ APNs Device Token (HEX): xxx
✅ FCM TOKEN: xxx
📬 Notification received (foreground): {...}
📬 Notification tapped: {...}
Enter fullscreen mode Exit fullscreen mode

Android - Logcat:

Filter by: FirebaseMessaging
Look for token generation and notification delivery logs
Enter fullscreen mode Exit fullscreen mode

Common Issues and Solutions

Issue 1: "0 sends" in Firebase Console

Symptom: Firebase Console shows "Completed" with 0 sends

Cause: APNs Authentication Keys configured instead of Certificates

Solution:

  1. Create APNs Certificate (.p12) as described above
  2. Upload to Firebase Console → APNs Certificates section
  3. DELETE all APNs Authentication Keys
  4. Retry sending notification

Reference: GitHub Issue firebase/firebase-ios-sdk#4042


Issue 2: iOS Notifications Not Appearing

Checklist:

  • ✅ Physical device (not simulator)
  • ✅ APNs Certificate uploaded to Firebase
  • ✅ APNs Authentication Keys deleted
  • ✅ Certificate not expired
  • ✅ Push Notifications capability enabled in Xcode
  • ✅ Background Modes → Remote notifications enabled
  • ✅ Device notification permissions granted
  • GoogleService-Info.plist in Xcode project
  • ✅ Bundle ID matches Firebase and Apple Developer

Debug Command:

# Check APNs token in Xcode console
# Should see: "✅ APNs Device Token (HEX): xxx"
Enter fullscreen mode Exit fullscreen mode

Issue 3: FCM Token is null/undefined

Causes:

  1. Firebase not initialized: Check GoogleService-Info.plist / google-services.json is present
  2. Permissions denied: User declined notification permission
  3. Network issue: Device offline during token generation
  4. Google Play Services (Android): Update required

Solution:

// Add error handling
try {
  const { token } = await FirebaseMessaging.getToken();
  if (!token) {
    console.error('FCM token is null - check Firebase configuration');
    return;
  }
  console.log('FCM Token:', token);
} catch (error) {
  console.error('Error getting FCM token:', error);
}
Enter fullscreen mode Exit fullscreen mode

Issue 4: Notifications Work Sometimes

Cause: Token refresh not handled

Solution: Implement tokenReceived listener:

FirebaseMessaging.addListener('tokenReceived', async (event) => {
  console.log('Token refreshed:', event.token);
  await this.registerTokenWithBackend(event.token);
});
Enter fullscreen mode Exit fullscreen mode

FCM tokens can be refreshed by Firebase SDK periodically or after app reinstall.


Issue 5: Android Notifications Don't Vibrate/Sound

Cause: Notification channel not configured

Solution: Configure Android notification channel in backend:

Android = new AndroidConfig
{
    Priority = Priority.High,
    Notification = new AndroidNotification
    {
        Sound = "default",
        ChannelId = "high_importance_channel",  // Must match app's channel
        DefaultSound = true,
        DefaultVibrateTimings = true
    }
}
Enter fullscreen mode Exit fullscreen mode

Or create channel in Android app (optional):

// Android native code (MainActivity.kt)
val channel = NotificationChannel(
    "high_importance_channel",
    "Important Notifications",
    NotificationManager.IMPORTANCE_HIGH
).apply {
    description = "Channel for important notifications"
    enableVibration(true)
    enableLights(true)
}
notificationManager.createNotificationChannel(channel)
Enter fullscreen mode Exit fullscreen mode

Production Considerations

1. Certificate Expiration Management

APNs Certificates expire after 1 year.

Strategy:

// Backend: Track certificate expiration
interface ApnsCertificate {
  id: number;
  certificatePath: string;
  password: string;
  expirationDate: Date;
  notificationSentAt?: Date;
}

// Cron job: Check 30 days before expiration
async function checkCertificateExpiration() {
  const certificates = await db.query(
    'SELECT * FROM apns_certificates WHERE expiration_date <= NOW() + INTERVAL \'30 days\''
  );

  for (const cert of certificates) {
    // Send email alert to admin
    await sendAdminAlert(`APNs certificate expires on ${cert.expirationDate}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Renewal Reminder: Set calendar reminder for March 2027 (or 1 month before expiration).


2. Token Management Best Practices

Database Schema:

-- Track token lifecycle
ALTER TABLE push_tokens ADD COLUMN last_used_at TIMESTAMP;
ALTER TABLE push_tokens ADD COLUMN error_count INTEGER DEFAULT 0;
ALTER TABLE push_tokens ADD COLUMN is_valid BOOLEAN DEFAULT true;

-- Cleanup old tokens (cron job)
DELETE FROM push_tokens
WHERE last_used_at < NOW() - INTERVAL '90 days';
Enter fullscreen mode Exit fullscreen mode

Handle Invalid Tokens:

try {
    await FirebaseMessaging.DefaultInstance.SendAsync(message);
    await UpdateTokenLastUsed(token);
} catch (FirebaseMessagingException ex) {
    if (ex.ErrorCode == MessagingErrorCode.Unregistered ||
        ex.ErrorCode == MessagingErrorCode.InvalidArgument) {
        // Token is invalid - remove from database
        await DeleteToken(token);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Notification Delivery Tracking

Add analytics:

public class NotificationLog
{
    public int Id { get; set; }
    public Guid UserId { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
    public DateTime SentAt { get; set; }
    public DateTime? DeliveredAt { get; set; }
    public DateTime? OpenedAt { get; set; }
    public string? ErrorMessage { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Track in Angular:

FirebaseMessaging.addListener('notificationActionPerformed', async (event) => {
  // Track notification open
  await this.http.post('/api/analytics/notification-opened', {
    notificationId: event.notification.data?.notificationId,
    timestamp: new Date().toISOString()
  }).toPromise();
});
Enter fullscreen mode Exit fullscreen mode

4. Rate Limiting

Prevent notification spam:

public class NotificationRateLimiter
{
    private readonly Dictionary<Guid, Queue<DateTime>> _userNotifications = new();
    private readonly int _maxNotificationsPerHour = 10;

    public bool CanSendNotification(Guid userId)
    {
        if (!_userNotifications.ContainsKey(userId))
        {
            _userNotifications[userId] = new Queue<DateTime>();
        }

        var queue = _userNotifications[userId];
        var oneHourAgo = DateTime.UtcNow.AddHours(-1);

        // Remove notifications older than 1 hour
        while (queue.Count > 0 && queue.Peek() < oneHourAgo)
        {
            queue.Dequeue();
        }

        if (queue.Count >= _maxNotificationsPerHour)
        {
            return false; // Rate limit exceeded
        }

        queue.Enqueue(DateTime.UtcNow);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

5. User Preferences

Allow users to control notifications:

// Settings UI
interface NotificationPreferences {
  enablePushNotifications: boolean;
  notifyOnNewDocuments: boolean;
  notifyOnComments: boolean;
  notifyOnUpdates: boolean;
  quietHoursStart?: string; // "22:00"
  quietHoursEnd?: string;   // "08:00"
}

// Backend: Check preferences before sending
async function sendNotificationIfAllowed(
  userId: Guid,
  notificationType: string
) {
  const prefs = await getUserNotificationPreferences(userId);

  if (!prefs.enablePushNotifications) {
    return false; // User disabled all notifications
  }

  if (notificationType === 'new_document' && !prefs.notifyOnNewDocuments) {
    return false;
  }

  if (isQuietHours(prefs)) {
    return false; // User in quiet hours
  }

  return await sendNotification(userId, ...);
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing Firebase push notifications in a Capacitor Angular app requires careful attention to platform-specific requirements, particularly on iOS where the choice between APNs Certificates and Authentication Keys can make or break your implementation.

Key Takeaways

  1. Use APNs Certificates (.p12) for iOS, not Authentication Keys - they're more reliable with Firebase Console
  2. Test on physical devices - iOS Simulator doesn't support push notifications
  3. Implement token refresh - FCM tokens can change, handle tokenReceived event
  4. Handle all notification states - foreground, background, and killed app
  5. Track certificate expiration - APNs Certificates expire yearly
  6. Store tokens securely - associate with user accounts in your backend
  7. Respect user preferences - allow users to control notification types and timing
  8. Monitor delivery - track sent, delivered, and opened notifications

What We Built

✅ Complete Firebase project setup
✅ iOS APNs Certificate configuration
✅ iOS native implementation (Swift)
✅ Android Firebase integration
✅ Cross-platform Angular service
✅ Backend integration with Firebase Admin SDK
✅ Testing strategies
✅ Production-ready error handling

Next Steps

  • Implement deep linking for notification actions
  • Add rich notifications with images/actions
  • Create notification categories/channels
  • Build admin panel for sending custom notifications
  • Add A/B testing for notification content
  • Implement notification scheduling

Resources


Published: February 16, 2026
Author: Václav Švára
Generated with assistance from: Claude (Anthropic AI)
License: MIT


Questions or issues? Share your experience in the comments below!

Top comments (0)