Published: February 16, 2026
Author: Václav Švára
Generated with assistance from: Claude (Anthropic AI)
Table of Contents
- Introduction
- Architecture Overview
- Prerequisites
- Firebase Project Setup
- iOS Configuration
- Android Configuration
- Angular Service Implementation
- Backend Integration
- Testing Push Notifications
- Common Issues and Solutions
- Production Considerations
- Conclusion
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 │
└─────────────────┘
Token Flow
- App Launch: Angular service initializes notification system
- Permission Request: User grants notification permission
-
Native Token Generation:
- iOS: APNs generates device token
- Android: FCM generates token directly
- FCM Token Creation: Firebase SDK converts platform tokens to FCM tokens
- Token Storage: Angular service receives FCM token
- Backend Registration: Token sent to your backend API
- Notification Sending: Backend sends notification via Firebase Admin SDK
- Platform Routing: FCM routes to APNs (iOS) or FCM (Android)
- 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+)
Required Packages
# Firebase Messaging plugin for Capacitor
npm install @capacitor-firebase/messaging
# Capacitor platforms
npm install @capacitor/ios @capacitor/android
Firebase Project Setup
Step 1: Create Firebase Project
- Go to Firebase Console
- Click "Add project"
- Enter project details:
Project name: YourApp
Enable Google Analytics: Yes (recommended)
- Click "Create project"
Step 2: Add iOS App
- In Firebase Console → Project Overview → Add app → iOS
- Enter configuration:
iOS bundle ID: com.yourcompany.yourapp
App nickname: YourApp iOS (optional)
App Store ID: Leave empty
- Download
GoogleService-Info.plist - Save file for later - you'll add it to Xcode project
Step 3: Add Android App
- In Firebase Console → Project Overview → Add app → Android
- Enter configuration:
Android package name: com.yourcompany.yourapp
App nickname: YourApp Android (optional)
SHA-1: Optional for testing
- Download
google-services.json - Save file for later - you'll add it to Android project
Step 4: Enable Cloud Messaging
- Firebase Console → Project Settings → Cloud Messaging
- Note the Server Key (for backend integration)
- 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
- Go to Apple Developer Console
- Click "+" → "App IDs" → "App"
- Configure:
Description: YourApp
Bundle ID: com.yourcompany.yourapp (Explicit)
Capabilities: Enable "Push Notifications"
- Click "Register"
Step 2: Generate Certificate Signing Request (CSR)
On your Mac:
- Open Keychain Access (Applications → Utilities)
- Menu: Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority...
- Fill in:
User Email Address: your.email@example.com
Common Name: YourApp Push Certificate
CA Email Address: Leave empty
Request is: Saved to disk
- Check "Let me specify key pair information"
- Click "Continue"
- Save as:
YourAppPush.certSigningRequest - Key Pair Information:
Key Size: 2048 bits
Algorithm: RSA
- Click "Continue" → "Done"
Step 3: Create APNs Certificate
- Go to Apple Developer Console
- Click "+" to create new certificate
- Select "Apple Push Notification service SSL (Sandbox & Production)"
- Click "Continue"
- Select your App ID:
com.yourcompany.yourapp - Click "Continue"
- Upload the CSR file:
YourAppPush.certSigningRequest - Click "Continue"
- Download the certificate:
aps.cer
Step 4: Install Certificate to Keychain
-
Double-click
aps.cerfile - Certificate automatically installs to Keychain Access
- Open Keychain Access → login keychain → My Certificates
- Find "Apple Push Services: com.yourcompany.yourapp"
- Expand it (click triangle) → verify private key is underneath
Step 5: Export as .p12 File
- In Keychain Access, select the certificate (top level, not private key)
- Right-click → "Export..."
- Configure export:
File Format: Personal Information Exchange (.p12)
Save As: YourAppPushCert.p12
- Enter a password (remember this! e.g.,
YourApp2026) - Click "Save"
- Enter your Mac password to allow export
You now have YourAppPushCert.p12 ready for Firebase.
Step 6: Upload to Firebase Console
- Firebase Console → ⚙️ Settings → Project settings → Cloud Messaging
- Scroll to "Apple app configuration"
- Expand "APNs Certificates" section (NOT Authentication Key!)
- Click "Upload" for Production certificate:
Certificate file: YourAppPushCert.p12
Password: YourApp2026 (your password)
- 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!
- In same "Apple app configuration" section
- Find "APNs Authentication Key" section
- Click "Delete" for ALL keys (Development and Production)
- 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
In Xcode:
- Right-click App folder (under App.xcodeproj)
- Select "Add Files to 'App'..."
- Select
GoogleService-Info.plist - Check "Copy items if needed"
- Target: Check "App"
- 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
Install pods:
cd ios/App
pod install
IMPORTANT: Always open .xcworkspace file after installing pods:
open App.xcworkspace
Step 3: Enable Capabilities in Xcode
- Select App target
- Go to "Signing & Capabilities" tab
- Click "+ Capability"
- Add "Push Notifications"
- 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>
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)
}
}
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
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
}
}
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'
}
Step 3: Sync Capacitor
npx cap sync android
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');
}
}
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();
}
});
}
}
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);
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'
}
Install Firebase Admin SDK
For .NET Backend:
dotnet add package FirebaseAdmin
For Node.js Backend:
npm install firebase-admin
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;
}
}
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" }
}
);
Testing Push Notifications
Test from Firebase Console
- Firebase Console → Engage → Messaging
- Click "Create your first campaign" → "Firebase Notification messages"
- Fill in notification:
Title: Test Notification
Text: This is a test push notification
- Click "Send test message"
- Paste FCM token (from Xcode/Android Studio logs or Angular service)
- Click "Test"
Expected Result: Notification appears on device
Test Scenarios
1. Foreground Notification (App Open)
- App is running and visible
- Notification should trigger
notificationReceivedlistener - iOS: Banner shows by default (due to
completionHandlerin 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
notificationActionPerformedlistener - 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: {...}
Android - Logcat:
Filter by: FirebaseMessaging
Look for token generation and notification delivery logs
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:
- Create APNs Certificate (.p12) as described above
- Upload to Firebase Console → APNs Certificates section
- DELETE all APNs Authentication Keys
- 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.plistin Xcode project - ✅ Bundle ID matches Firebase and Apple Developer
Debug Command:
# Check APNs token in Xcode console
# Should see: "✅ APNs Device Token (HEX): xxx"
Issue 3: FCM Token is null/undefined
Causes:
-
Firebase not initialized: Check
GoogleService-Info.plist/google-services.jsonis present - Permissions denied: User declined notification permission
- Network issue: Device offline during token generation
- 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);
}
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);
});
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
}
}
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)
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}`);
}
}
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';
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);
}
}
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; }
}
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();
});
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;
}
}
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, ...);
}
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
- Use APNs Certificates (.p12) for iOS, not Authentication Keys - they're more reliable with Firebase Console
- Test on physical devices - iOS Simulator doesn't support push notifications
-
Implement token refresh - FCM tokens can change, handle
tokenReceivedevent - Handle all notification states - foreground, background, and killed app
- Track certificate expiration - APNs Certificates expire yearly
- Store tokens securely - associate with user accounts in your backend
- Respect user preferences - allow users to control notification types and timing
- 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
- Firebase Cloud Messaging Documentation
- Capacitor Firebase Messaging Plugin
- Apple Push Notification Service
- GitHub Issue #4042 - APNs Auth Key Bug
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)