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
- System Goal
- Final Architecture
- End-to-End Flow
- Project Folder Structure
- Firebase Setup
- Flutter Setup
- Flutter File-by-File Implementation
- PHP Backend Setup
- PHP File-by-File Implementation
- Database Structure
- API Testing
- Notification Lifecycle Handling
- Broadcast Notifications
- Scheduled Notifications
- JSON/MD Content-Driven Notifications
- Production Hardening
- Implementation Sequence
- Common Problems and Fixes
- Final Senior Notes
1. System Goal
We want a full notification system where:
- Flutter app initializes Firebase
- Device gets FCM token
- Flutter sends token to PHP backend
- PHP stores token in database
- PHP sends single or broadcast notifications through Firebase HTTP v1
- Flutter receives notification
- Flutter handles:
- foreground
- background
- terminated state
- User taps notification
- 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
- Go to Firebase Console
- Create a project
- Add Android app
- Download
google-services.json - 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:
- Create service account
- Download JSON key
- 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
- Create Firebase project
- Add Android app
- Put
google-services.json - Add Flutter packages
- Initialize Firebase
- Setup local notifications
Phase 2: Routing + lifecycle
- Create navigator key
- Create app routes
- Create notification router
- Handle foreground
- Handle background
- Handle terminated open
Phase 3: Backend token system
- Create DB table
- Create register token API
- Sync token from Flutter
- Confirm token stored in DB
Phase 4: Sending system
- Setup service account
- Install
google/auth - Create Firebase auth helper
- Create Firebase sender
- Create send single API
- Test from Postman
Phase 5: Production features
- Create broadcast API
- Create scheduled cron
- Create JSON-based notification source
- Add logs
- 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:
- token registration
- single notification send
- click opens correct route
Then add:
- broadcast
- scheduler
- content-driven notification engine
- 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