Flutterfirebasephp

Full Phase Notification System Guide

Firebase + Flutter + PHP Backend

Production-Level Architecture Tutorial in One MD File

This guide gives you a full-phase, end-to-end notification system using:

  • Firebase Cloud Messaging (FCM)
  • Flutter App
  • PHP Backend
  • MySQL for token storage
  • JSON-based content integration if needed

This is written as a senior architecture guide but in a practical way so you can implement it step by step.


Table of Contents

  1. System Goal
  2. Final Architecture
  3. End-to-End Flow
  4. Project Folder Structure
  5. Firebase Setup
  6. Flutter Setup
  7. Flutter File-by-File Implementation
  8. PHP Backend Setup
  9. PHP File-by-File Implementation
  10. Database Structure
  11. API Testing
  12. Notification Lifecycle Handling
  13. Broadcast Notifications
  14. Scheduled Notifications
  15. JSON/MD Content-Driven Notifications
  16. Production Hardening
  17. Implementation Sequence
  18. Common Problems and Fixes
  19. Final Senior Notes

1. System Goal

We want a full notification system where:

  1. Flutter app initializes Firebase
  2. Device gets FCM token
  3. Flutter sends token to PHP backend
  4. PHP stores token in database
  5. PHP sends single or broadcast notifications through Firebase HTTP v1
  6. Flutter receives notification
  7. Flutter handles:
    • foreground
    • background
    • terminated state
  8. User taps notification
  9. App opens the correct screen dynamically

2. Final Architecture

Admin / Content Trigger / Cron / Business Logic
                    ↓
                PHP Backend
                    ↓
    Firebase HTTP v1 API (OAuth Access Token)
                    ↓
       Firebase Cloud Messaging (FCM)
                    ↓
               Flutter App
                    ↓
     Local Notification + Dynamic Router
                    ↓
      Correct Screen / Correct Content Page

3. End-to-End Flow

3.1 Token Registration Flow

Flutter App Start
    ↓
Firebase initializes
    ↓
FCM token generated
    ↓
Flutter sends token to PHP backend
    ↓
PHP stores token in DB

3.2 Notification Sending Flow

Admin / API / Scheduler
    ↓
PHP builds notification payload
    ↓
PHP requests OAuth token from Google
    ↓
PHP sends request to Firebase HTTP v1
    ↓
FCM delivers to device
    ↓
Flutter handles message
    ↓
User taps notification
    ↓
App opens target route

4. Project Folder Structure

4.1 Flutter

lib/
├── app/
│   ├── app.dart
│   ├── app_routes.dart
│   └── navigator_key.dart
│
├── core/
│   ├── network/
│   │   └── api_client.dart
│   │
│   ├── storage/
│   │   └── local_storage_service.dart
│   │
│   └── notification/
│       ├── notification_service.dart
│       ├── local_notification_service.dart
│       ├── notification_router.dart
│       ├── notification_payload.dart
│       └── token_sync_service.dart
│
├── features/
│   ├── home/
│   │   └── presentation/home_screen.dart
│   ├── notifications/
│   │   └── presentation/notification_list_screen.dart
│   └── content/
│       └── presentation/content_detail_screen.dart
│
└── main.dart

4.2 PHP Backend

backend/
├── api/
│   ├── register_token.php
│   ├── unregister_token.php
│   ├── send_notification.php
│   ├── send_broadcast.php
│   └── health.php
│
├── config/
│   ├── app.php
│   ├── database.php
│   └── firebase.php
│
├── core/
│   ├── db.php
│   ├── response.php
│   ├── firebase_auth.php
│   ├── firebase_sender.php
│   └── logger.php
│
├── modules/
│   ├── tokens/
│   │   └── token_repository.php
│   └── notifications/
│       └── notification_builder.php
│
├── cron/
│   └── scheduled_notifications.php
│
├── data/
│   ├── notifications.json
│   └── content/
│
├── storage/
│   └── logs/
│
├── vendor/
└── firebase-service-account.json

5. Firebase Setup

5.1 Create Firebase project

  1. Go to Firebase Console
  2. Create a project
  3. Add Android app
  4. Download google-services.json
  5. Place it here:
android/app/google-services.json

5.2 Enable Cloud Messaging

Inside Firebase project, make sure Cloud Messaging is enabled.

5.3 Service Account for PHP

From Firebase / Google Cloud service accounts:

  1. Create service account
  2. Download JSON key
  3. Save it in backend:
backend/firebase-service-account.json

This file will be used by PHP to get OAuth access token for Firebase HTTP v1.


6. Flutter Setup

6.1 pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^3.13.0
  firebase_messaging: ^15.2.5
  flutter_local_notifications: ^17.2.1+2
  shared_preferences: ^2.5.3
  http: ^1.2.1

Use the latest compatible versions in your real project.


6.2 Android Gradle setup

android/build.gradle.kts

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

android/app/build.gradle.kts

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("dev.flutter.flutter-gradle-plugin")
    id("com.google.gms.google-services")
}

7. Flutter File-by-File Implementation

7.1 main.dart

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'app/app.dart';
import 'core/notification/local_notification_service.dart';
import 'core/notification/notification_service.dart';

Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  await NotificationService.handleBackgroundMessage(message);
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);

  await LocalNotificationService.initialize();
  await NotificationService.initialize();

  runApp(const MyApp());
}

7.2 app/navigator_key.dart

import 'package:flutter/material.dart';

final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

7.3 app/app.dart

import 'package:flutter/material.dart';
import 'app_routes.dart';
import 'navigator_key.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      routes: AppRoutes.routes,
    );
  }
}

7.4 app/app_routes.dart

import 'package:flutter/material.dart';
import '../features/home/presentation/home_screen.dart';
import '../features/notifications/presentation/notification_list_screen.dart';
import '../features/content/presentation/content_detail_screen.dart';

class AppRoutes {
  static Map<String, WidgetBuilder> routes = {
    '/': (_) => const HomeScreen(),
    '/notifications': (_) => const NotificationListScreen(),
    '/content-detail': (_) => const ContentDetailScreen(),
  };
}

7.5 core/notification/notification_payload.dart

class NotificationPayload {
  final String title;
  final String body;
  final String route;
  final String? contentType;
  final String? contentId;
  final String? channel;
  final String? image;

  NotificationPayload({
    required this.title,
    required this.body,
    required this.route,
    this.contentType,
    this.contentId,
    this.channel,
    this.image,
  });

  factory NotificationPayload.fromMap(Map<String, dynamic> map) {
    return NotificationPayload(
      title: map['title']?.toString() ?? '',
      body: map['body']?.toString() ?? '',
      route: map['route']?.toString() ?? '/',
      contentType: map['content_type']?.toString(),
      contentId: map['content_id']?.toString(),
      channel: map['channel']?.toString(),
      image: map['image']?.toString(),
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'title': title,
      'body': body,
      'route': route,
      'content_type': contentType,
      'content_id': contentId,
      'channel': channel,
      'image': image,
    };
  }
}

7.6 core/notification/notification_router.dart

import '../../app/navigator_key.dart';

class NotificationRouter {
  static void handleData(Map<String, dynamic> data) {
    final route = data['route']?.toString() ?? '/';
    final contentId = data['content_id']?.toString();
    final contentType = data['content_type']?.toString();

    if (route == '/content-detail') {
      navigatorKey.currentState?.pushNamed(
        '/content-detail',
        arguments: {
          'content_id': contentId,
          'content_type': contentType,
        },
      );
      return;
    }

    navigatorKey.currentState?.pushNamed(route, arguments: data);
  }
}

7.7 core/notification/local_notification_service.dart

import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'notification_router.dart';

class LocalNotificationService {
  static final FlutterLocalNotificationsPlugin _plugin =
      FlutterLocalNotificationsPlugin();

  static Future<void> initialize() async {
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');

    const settings = InitializationSettings(
      android: androidSettings,
    );

    await _plugin.initialize(
      settings,
      onDidReceiveNotificationResponse: (NotificationResponse response) {
        final payload = response.payload;
        if (payload != null && payload.isNotEmpty) {
          final data = jsonDecode(payload) as Map<String, dynamic>;
          NotificationRouter.handleData(data);
        }
      },
    );
  }

  static Future<void> show(RemoteMessage message) async {
    final id = DateTime.now().millisecondsSinceEpoch.remainder(100000);

    final title =
        message.notification?.title ?? message.data['title']?.toString() ?? '';
    final body =
        message.notification?.body ?? message.data['body']?.toString() ?? '';

    const androidDetails = AndroidNotificationDetails(
      'general_channel',
      'General Notifications',
      channelDescription: 'General app notifications',
      importance: Importance.max,
      priority: Priority.high,
    );

    const details = NotificationDetails(android: androidDetails);

    await _plugin.show(
      id,
      title,
      body,
      details,
      payload: jsonEncode(message.data),
    );
  }
}

7.8 core/network/api_client.dart

import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiClient {
  static const String baseUrl = 'https://your-domain.com/backend/api';

  static Future<Map<String, dynamic>> post(
    String endpoint,
    Map<String, dynamic> body,
  ) async {
    final response = await http.post(
      Uri.parse('$baseUrl$endpoint'),
      headers: {
        'Content-Type': 'application/json',
      },
      body: jsonEncode(body),
    );

    return jsonDecode(response.body) as Map<String, dynamic>;
  }
}

7.9 core/notification/token_sync_service.dart

import 'package:shared_preferences/shared_preferences.dart';
import '../network/api_client.dart';

class TokenSyncService {
  static Future<void> syncToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    final oldToken = prefs.getString('fcm_token');

    if (oldToken == token) {
      return;
    }

    final body = {
      'token': token,
      'platform': 'android',
      'app_version': '1.0.0',
    };

    await ApiClient.post('/register_token.php', body);

    await prefs.setString('fcm_token', token);
  }
}

7.10 core/notification/notification_service.dart

import 'package:firebase_messaging/firebase_messaging.dart';
import 'local_notification_service.dart';
import 'notification_router.dart';
import 'token_sync_service.dart';

class NotificationService {
  static final FirebaseMessaging _messaging = FirebaseMessaging.instance;

  static Future<void> initialize() async {
    await _requestPermission();
    await _setupToken();
    await _setupForegroundListener();
    await _setupOpenedAppListener();
    await _handleInitialMessage();
  }

  static Future<void> _requestPermission() async {
    await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false,
    );
  }

  static Future<void> _setupToken() async {
    final token = await _messaging.getToken();
    if (token != null) {
      await TokenSyncService.syncToken(token);
    }

    _messaging.onTokenRefresh.listen((newToken) async {
      await TokenSyncService.syncToken(newToken);
    });
  }

  static Future<void> _setupForegroundListener() async {
    FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
      await LocalNotificationService.show(message);
    });
  }

  static Future<void> _setupOpenedAppListener() async {
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      NotificationRouter.handleData(message.data);
    });
  }

  static Future<void> _handleInitialMessage() async {
    final message = await _messaging.getInitialMessage();
    if (message != null) {
      NotificationRouter.handleData(message.data);
    }
  }

  static Future<void> handleBackgroundMessage(RemoteMessage message) async {
    // Optional: store analytics, log event, cache notification
  }
}

7.11 Minimal screen examples

home_screen.dart

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: Text('Home Screen')),
    );
  }
}

notification_list_screen.dart

import 'package:flutter/material.dart';

class NotificationListScreen extends StatelessWidget {
  const NotificationListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: Text('Notification List Screen')),
    );
  }
}

content_detail_screen.dart

import 'package:flutter/material.dart';

class ContentDetailScreen extends StatelessWidget {
  const ContentDetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final args =
        ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;

    return Scaffold(
      body: Center(
        child: Text(
          'Content Detail: ${args?['content_type']} / ${args?['content_id']}',
        ),
      ),
    );
  }
}

8. PHP Backend Setup

8.1 Install composer dependency

Run in backend folder:

composer require google/auth

This is the recommended production approach for Firebase HTTP v1 token generation.


8.2 config/firebase.php

<?php

return [
    'project_id' => 'your-firebase-project-id',
    'service_account_path' => __DIR__ . '/../firebase-service-account.json',
];

8.3 config/database.php

<?php

define('DB_HOST', 'localhost');
define('DB_NAME', 'your_database');
define('DB_USER', 'root');
define('DB_PASS', '');

8.4 core/db.php

<?php

require_once __DIR__ . '/../config/database.php';

class DB {
    public static function connect(): PDO {
        static $pdo = null;

        if ($pdo === null) {
            $pdo = new PDO(
                'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
                DB_USER,
                DB_PASS,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                ]
            );
        }

        return $pdo;
    }
}

8.5 core/response.php

<?php

function json_response($data, int $status = 200): void {
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

8.6 core/logger.php

<?php

function app_log(string $message): void {
    $dir = __DIR__ . '/../storage/logs';
    if (!is_dir($dir)) {
        mkdir($dir, 0777, true);
    }

    $file = $dir . '/app.log';
    $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;
    file_put_contents($file, $line, FILE_APPEND);
}

8.7 core/firebase_auth.php

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Google\Auth\Credentials\ServiceAccountCredentials;

function get_firebase_access_token(): string {
    $config = require __DIR__ . '/../config/firebase.php';

    $scopes = ['https://www.googleapis.com/auth/firebase.messaging'];

    $credentials = new ServiceAccountCredentials(
        $scopes,
        $config['service_account_path']
    );

    $token = $credentials->fetchAuthToken();

    if (!isset($token['access_token'])) {
        throw new Exception('Unable to generate Firebase access token');
    }

    return $token['access_token'];
}

8.8 core/firebase_sender.php

<?php

require_once __DIR__ . '/firebase_auth.php';

function send_firebase_message(array $message): array {
    $config = require __DIR__ . '/../config/firebase.php';
    $accessToken = get_firebase_access_token();

    $url = 'https://fcm.googleapis.com/v1/projects/' . $config['project_id'] . '/messages:send';

    $payload = [
        'message' => $message,
    ];

    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);

    curl_close($ch);

    if ($curlError) {
        return [
            'success' => false,
            'message' => $curlError,
        ];
    }

    return [
        'success' => $httpCode >= 200 && $httpCode < 300,
        'http_code' => $httpCode,
        'response' => json_decode($response, true),
        'raw_response' => $response,
    ];
}

9. Database Structure

9.1 notification_tokens table

CREATE TABLE IF NOT EXISTS notification_tokens (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NULL,
    token VARCHAR(255) NOT NULL UNIQUE,
    platform VARCHAR(50) DEFAULT 'android',
    app_version VARCHAR(50) NULL,
    is_active TINYINT(1) DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

This table is enough for token storage in the first version.


10. PHP File-by-File Implementation

10.1 modules/tokens/token_repository.php

<?php

require_once __DIR__ . '/../../core/db.php';

class TokenRepository {
    public function saveOrUpdate(?int $userId, string $token, string $platform, ?string $appVersion): bool {
        $pdo = DB::connect();

        $sql = "
            INSERT INTO notification_tokens (user_id, token, platform, app_version, is_active)
            VALUES (:user_id, :token, :platform, :app_version, 1)
            ON DUPLICATE KEY UPDATE
                user_id = VALUES(user_id),
                platform = VALUES(platform),
                app_version = VALUES(app_version),
                is_active = 1,
                updated_at = CURRENT_TIMESTAMP
        ";

        $stmt = $pdo->prepare($sql);

        return $stmt->execute([
            ':user_id' => $userId,
            ':token' => $token,
            ':platform' => $platform,
            ':app_version' => $appVersion,
        ]);
    }

    public function getActiveTokens(): array {
        $pdo = DB::connect();
        $stmt = $pdo->query("SELECT token FROM notification_tokens WHERE is_active = 1");
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
    }

    public function deactivateToken(string $token): bool {
        $pdo = DB::connect();
        $stmt = $pdo->prepare("UPDATE notification_tokens SET is_active = 0 WHERE token = :token");
        return $stmt->execute([':token' => $token]);
    }
}

10.2 modules/notifications/notification_builder.php

<?php

class NotificationBuilder {
    public static function buildSingleTokenMessage(
        string $token,
        string $title,
        string $body,
        array $data = []
    ): array {
        $stringData = [];

        foreach ($data as $key => $value) {
            $stringData[$key] = (string)$value;
        }

        $stringData['title'] = $title;
        $stringData['body'] = $body;

        return [
            'token' => $token,
            'notification' => [
                'title' => $title,
                'body' => $body,
            ],
            'data' => $stringData,
            'android' => [
                'priority' => 'HIGH',
            ],
        ];
    }
}

10.3 api/register_token.php

<?php

require_once __DIR__ . '/../core/response.php';
require_once __DIR__ . '/../modules/tokens/token_repository.php';

$input = json_decode(file_get_contents('php://input'), true);

$token = trim($input['token'] ?? '');
$platform = trim($input['platform'] ?? 'android');
$appVersion = trim($input['app_version'] ?? '');
$userId = isset($input['user_id']) ? (int)$input['user_id'] : null;

if ($token === '') {
    json_response([
        'success' => false,
        'message' => 'Token is required',
    ], 422);
}

$repo = new TokenRepository();
$repo->saveOrUpdate($userId, $token, $platform, $appVersion);

json_response([
    'success' => true,
    'message' => 'Token registered successfully',
]);

10.4 api/unregister_token.php

<?php

require_once __DIR__ . '/../core/response.php';
require_once __DIR__ . '/../modules/tokens/token_repository.php';

$input = json_decode(file_get_contents('php://input'), true);

$token = trim($input['token'] ?? '');

if ($token === '') {
    json_response([
        'success' => false,
        'message' => 'Token is required',
    ], 422);
}

$repo = new TokenRepository();
$repo->deactivateToken($token);

json_response([
    'success' => true,
    'message' => 'Token deactivated successfully',
]);

10.5 api/health.php

<?php

require_once __DIR__ . '/../core/response.php';

json_response([
    'success' => true,
    'message' => 'Notification backend is healthy',
]);

10.6 api/send_notification.php

<?php

require_once __DIR__ . '/../core/response.php';
require_once __DIR__ . '/../core/firebase_sender.php';
require_once __DIR__ . '/../modules/notifications/notification_builder.php';

$input = json_decode(file_get_contents('php://input'), true);

$token = trim($input['token'] ?? '');
$title = trim($input['title'] ?? '');
$body = trim($input['body'] ?? '');
$route = trim($input['route'] ?? '/');
$contentType = trim($input['content_type'] ?? '');
$contentId = trim($input['content_id'] ?? '');
$channel = trim($input['channel'] ?? 'general');

if ($token === '' || $title === '' || $body === '') {
    json_response([
        'success' => false,
        'message' => 'token, title and body are required',
    ], 422);
}

$message = NotificationBuilder::buildSingleTokenMessage(
    $token,
    $title,
    $body,
    [
        'route' => $route,
        'content_type' => $contentType,
        'content_id' => $contentId,
        'channel' => $channel,
    ]
);

$result = send_firebase_message($message);

json_response($result, $result['success'] ? 200 : 500);

10.7 api/send_broadcast.php

<?php

require_once __DIR__ . '/../core/response.php';
require_once __DIR__ . '/../core/firebase_sender.php';
require_once __DIR__ . '/../modules/tokens/token_repository.php';
require_once __DIR__ . '/../modules/notifications/notification_builder.php';

$input = json_decode(file_get_contents('php://input'), true);

$title = trim($input['title'] ?? '');
$body = trim($input['body'] ?? '');
$route = trim($input['route'] ?? '/');
$contentType = trim($input['content_type'] ?? '');
$contentId = trim($input['content_id'] ?? '');
$channel = trim($input['channel'] ?? 'general');

if ($title === '' || $body === '') {
    json_response([
        'success' => false,
        'message' => 'title and body are required',
    ], 422);
}

$repo = new TokenRepository();
$tokens = $repo->getActiveTokens();

$results = [];

foreach ($tokens as $token) {
    $message = NotificationBuilder::buildSingleTokenMessage(
        $token,
        $title,
        $body,
        [
            'route' => $route,
            'content_type' => $contentType,
            'content_id' => $contentId,
            'channel' => $channel,
        ]
    );

    $results[] = send_firebase_message($message);
}

json_response([
    'success' => true,
    'total' => count($tokens),
    'results' => $results,
]);

11. API Testing

11.1 Register token

Endpoint

POST /backend/api/register_token.php

Body

{
  "token": "DEVICE_FCM_TOKEN",
  "platform": "android",
  "app_version": "1.0.0",
  "user_id": 12
}

11.2 Send single notification

Endpoint

POST /backend/api/send_notification.php

Body

{
  "token": "DEVICE_FCM_TOKEN",
  "title": "New Hadith",
  "body": "Tap to read now",
  "route": "/content-detail",
  "content_type": "hadith",
  "content_id": "HD-1001",
  "channel": "general"
}

11.3 Send broadcast

Endpoint

POST /backend/api/send_broadcast.php

Body

{
  "title": "Jumu'ah Reminder",
  "body": "Prepare for Jumu'ah prayer",
  "route": "/content-detail",
  "content_type": "article",
  "content_id": "JM-1",
  "channel": "general"
}

12. Notification Lifecycle Handling

Your app must handle all 3 states.

12.1 Foreground

When app is open:

  • FCM may not display system notification automatically
  • so Flutter shows it using flutter_local_notifications

Handled in:

FirebaseMessaging.onMessage.listen(...)

12.2 Background

When app is in background and user taps notification:

  • app opens
  • route is handled in:
FirebaseMessaging.onMessageOpenedApp.listen(...)

12.3 Terminated

When app is killed and opened by notification tap:

  • handled in:
FirebaseMessaging.instance.getInitialMessage()

13. Broadcast Notifications

Broadcast is used when the same notification should go to all active users.

Example:

  • New hadith published
  • New article published
  • app maintenance reminder
  • Islamic event announcement

Current implementation:

  • fetch active tokens
  • loop tokens
  • send one by one

This is simple and good for first production phase.

Later you can optimize with:

  • topic messaging
  • queue worker
  • chunked delivery

14. Scheduled Notifications

14.1 data/notifications.json

[
  {
    "title": "Today's Hadith",
    "body": "Tap to read today's hadith",
    "route": "/content-detail",
    "content_type": "hadith",
    "content_id": "HD-1001",
    "channel": "general",
    "active": true
  }
]

14.2 cron/scheduled_notifications.php

<?php

require_once __DIR__ . '/../core/firebase_sender.php';
require_once __DIR__ . '/../modules/tokens/token_repository.php';
require_once __DIR__ . '/../modules/notifications/notification_builder.php';

$file = __DIR__ . '/../data/notifications.json';

if (!file_exists($file)) {
    exit("notifications.json not found");
}

$items = json_decode(file_get_contents($file), true);

$repo = new TokenRepository();
$tokens = $repo->getActiveTokens();

foreach ($items as $item) {
    if (empty($item['active'])) {
        continue;
    }

    foreach ($tokens as $token) {
        $message = NotificationBuilder::buildSingleTokenMessage(
            $token,
            $item['title'],
            $item['body'],
            [
                'route' => $item['route'] ?? '/',
                'content_type' => $item['content_type'] ?? '',
                'content_id' => $item['content_id'] ?? '',
                'channel' => $item['channel'] ?? 'general',
            ]
        );

        send_firebase_message($message);
    }
}

14.3 Cron setup example

*/10 * * * * /usr/bin/php /path-to-project/backend/cron/scheduled_notifications.php

This means run every 10 minutes.


15. JSON / MD Content-Driven Notifications

If your app uses JSON files or markdown content files, notification payload can be generated from them.

Example content-driven use cases:

  • hadith of the day
  • Islamic name of the day
  • reminder content
  • new article release
  • event alerts

Recommended backend principle:

  • content source stays in JSON / MD
  • notification builder reads summary fields
  • backend sends only required route and IDs
  • Flutter loads full content after navigation

Do not send heavy full content inside FCM payload.

Good payload:

{
  "title": "New Hadith",
  "body": "Tap to read today's hadith",
  "route": "/content-detail",
  "content_type": "hadith",
  "content_id": "HD-1001"
}

Bad payload:

  • sending full HTML
  • sending full article body
  • sending huge JSON blocks

16. Production Hardening

16.1 Always use unique local notification IDs

Do not use same local notification ID every time. Otherwise old notification can be replaced by new one.

16.2 Sync token only if changed

Do not register token every app open if token is unchanged.

16.3 Listen for token refresh

FCM token can change.

Handled with:

_messaging.onTokenRefresh.listen(...)

16.4 Deactivate invalid tokens

If Firebase returns invalid token or unregistered token:

  • mark that token inactive in DB

16.5 Keep payload string-only

FCM data payload should be string-based.

That is why builder converts values to string.

16.6 Keep backend data-driven

Backend sends:

  • title
  • body
  • route
  • content_type
  • content_id

Flutter decides:

  • which page to open
  • how to render UI

16.7 Create separate channels later

For future:

  • general
  • adhan
  • reminder
  • emergency
  • update

16.8 Add logging

Every production system should log:

  • token registration
  • send attempts
  • failures
  • Firebase responses
  • invalid token cases

17. Implementation Sequence

This is the recommended order.

Phase 1: Firebase + Flutter foundation

  1. Create Firebase project
  2. Add Android app
  3. Put google-services.json
  4. Add Flutter packages
  5. Initialize Firebase
  6. Setup local notifications

Phase 2: Routing + lifecycle

  1. Create navigator key
  2. Create app routes
  3. Create notification router
  4. Handle foreground
  5. Handle background
  6. Handle terminated open

Phase 3: Backend token system

  1. Create DB table
  2. Create register token API
  3. Sync token from Flutter
  4. Confirm token stored in DB

Phase 4: Sending system

  1. Setup service account
  2. Install google/auth
  3. Create Firebase auth helper
  4. Create Firebase sender
  5. Create send single API
  6. Test from Postman

Phase 5: Production features

  1. Create broadcast API
  2. Create scheduled cron
  3. Create JSON-based notification source
  4. Add logs
  5. Deactivate dead tokens

18. Common Problems and Fixes

Problem 1: getToken() returns null

Possible reasons:

  • device not ready yet
  • Firebase setup incomplete
  • emulator issue
  • network issue

Fix:

  • test on real device
  • recheck Firebase configuration
  • ensure google-services.json package matches app package

Problem 2: Firebase send API returns auth error

Possible reasons:

  • wrong service account JSON
  • wrong project_id
  • missing google/auth package
  • invalid path to service account file

Fix:

  • verify service account JSON
  • verify backend/firebase-service-account.json path
  • verify Firebase project id

Problem 3: Notification arrives but click does not open screen

Possible reasons:

  • route missing
  • navigator key not attached
  • data payload missing route
  • getInitialMessage() not handled

Fix:

  • verify app routes
  • verify navigatorKey
  • verify router logic
  • test foreground/background/terminated separately

Problem 4: Foreground notification not visible

This is normal if you only rely on remote notification behavior.

Fix:

  • use flutter_local_notifications
  • show notification manually inside FirebaseMessaging.onMessage

Problem 5: Broadcast works slowly

This is expected if sending one by one for many users.

Fix later:

  • topic messaging
  • background queue
  • batch delivery strategy

Problem 6: Notification replaces previous one

Cause:

  • using fixed local notification ID like 0

Fix:

  • generate unique ID using timestamp

19. Final Senior Notes

A real notification system should be:

  • modular
  • clean
  • data-driven
  • easy to debug
  • safe to scale

Best responsibility split:

Flutter

  • initialize Firebase
  • receive messages
  • show local notifications
  • route user correctly

PHP

  • register tokens
  • manage active tokens
  • build payload
  • send through Firebase

Firebase

  • handle delivery

JSON / DB

  • store content source
  • store notification config
  • store registered tokens

This separation keeps the system maintainable.


Final Recommendation

First make these work end-to-end:

  1. token registration
  2. single notification send
  3. click opens correct route

Then add:

  1. broadcast
  2. scheduler
  3. content-driven notification engine
  4. analytics and admin panel

Optional Next Expansion

After this guide, a strong next step would be creating:

  • full Flutter ready-to-copy file pack
  • full PHP ready-to-copy file pack
  • database auto-create script in PHP
  • Postman collection
  • admin notification panel design
  • topic subscription system
  • custom sound notification channels
Previous Topic
0%