@flink-app/firebase-messaging-plugin 0.12.1-alpha.4 → 0.12.1-alpha.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
1
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredHandlers, HttpMethod } from "@flink-app/flink";
3
3
  import * as PostMessage_0 from "../src/handlers/PostMessage";
4
4
 
@@ -1,4 +1,4 @@
1
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
1
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredJobs } from "@flink-app/flink";
3
3
  export const jobs = [];
4
4
  autoRegisteredJobs.push(...jobs);
@@ -1,4 +1,4 @@
1
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
1
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredRepos } from "@flink-app/flink";
3
3
  export const repos = [];
4
4
  autoRegisteredRepos.push(...repos);
@@ -1,7 +1,7 @@
1
1
  import Message from "../../src/schemas/Message";
2
2
  import SendResult from "../../src/schemas/SendResult";
3
3
 
4
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
4
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
5
5
  export interface PostMessage_12_ReqSchema extends Message {}
6
6
 
7
7
  export interface PostMessage_12_ResSchema extends SendResult {}
package/.flink/start.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
1
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
2
2
  import "./generatedHandlers";
3
3
  import "./generatedRepos";
4
4
  import "./generatedJobs";
5
5
  import "../src/index";
6
+ export default {}; // Export an empty object to make it a module
@@ -24,7 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.handlers = void 0;
27
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
27
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
28
28
  var flink_1 = require("@flink-app/flink");
29
29
  var PostMessage_0 = __importStar(require("../src/handlers/PostMessage"));
30
30
  exports.handlers = [{ handler: PostMessage_0, assumedHttpMethod: flink_1.HttpMethod.post }];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.jobs = void 0;
4
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
4
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
5
5
  var flink_1 = require("@flink-app/flink");
6
6
  exports.jobs = [];
7
7
  flink_1.autoRegisteredJobs.push.apply(flink_1.autoRegisteredJobs, exports.jobs);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.repos = void 0;
4
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
4
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
5
5
  var flink_1 = require("@flink-app/flink");
6
6
  exports.repos = [];
7
7
  flink_1.autoRegisteredRepos.push.apply(flink_1.autoRegisteredRepos, exports.repos);
@@ -2,3 +2,5 @@ import "./generatedHandlers";
2
2
  import "./generatedRepos";
3
3
  import "./generatedJobs";
4
4
  import "../src/index";
5
+ declare const _default: {};
6
+ export default _default;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- // Generated Thu Mar 20 2025 15:57:55 GMT+0100 (Central European Standard Time)
3
+ // Generated Sun Nov 23 2025 14:24:04 GMT+0100 (Central European Standard Time)
4
4
  require("./generatedHandlers");
5
5
  require("./generatedRepos");
6
6
  require("./generatedJobs");
7
7
  require("../src/index");
8
+ exports.default = {}; // Export an empty object to make it a module
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@flink-app/firebase-messaging-plugin",
3
- "version": "0.12.1-alpha.4",
3
+ "version": "0.12.1-alpha.45",
4
4
  "description": "Flink plugin to send Firebase cloud messages",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\"",
7
7
  "build": "flink build",
8
- "prepublish": "npm run build",
8
+ "prepare": "npm run build",
9
9
  "watch": "nodemon --exec \"flink build\""
10
10
  },
11
11
  "author": "joel@frost.se",
@@ -16,11 +16,11 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@flink-app/management-api-plugin": "^0.12.1-alpha.4",
19
+ "@flink-app/management-api-plugin": "^0.12.1-alpha.45",
20
20
  "firebase-admin": "^12.1.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@flink-app/flink": "^0.12.1-alpha.4",
23
+ "@flink-app/flink": "^0.12.1-alpha.45",
24
24
  "@types/express": "^4.17.12",
25
25
  "@types/node": "22.13.10",
26
26
  "nodemon": "^2.0.7",
@@ -28,5 +28,5 @@
28
28
  "tsc-watch": "^4.2.9",
29
29
  "typescript": "5.4.5"
30
30
  },
31
- "gitHead": "8f06a4ab98e7179322d36546756d4305fd196f5a"
31
+ "gitHead": "af426a157217c110ac9c7beb48e2e746968bec33"
32
32
  }
package/readme.md CHANGED
@@ -1,79 +1,633 @@
1
- # Firebase messaging plugin
1
+ # Firebase Messaging Plugin
2
2
 
3
- **WORK IN PROGRESS 👷‍♀️👷🏻‍♂️**
3
+ A Flink plugin for sending push notifications using Firebase Cloud Messaging (FCM). Send notifications to individual devices or multiple devices with support for both notification and data messages.
4
4
 
5
- A FLINK plugin used for sending push notifications using Firebase (a.k.a. Firebase Cloud Messaging).
5
+ ## Installation
6
6
 
7
- ## Usage
7
+ Install the plugin to your Flink app project:
8
8
 
9
- Install plugin to your flink app project:
10
-
11
- ```
12
- npm i -S @flink-app/firebase-messaging-plugin
9
+ ```bash
10
+ npm install @flink-app/firebase-messaging-plugin
13
11
  ```
14
12
 
15
- Add and configure plugin in your app startup:
13
+ ## Configuration
16
14
 
17
- ```
15
+ ### Basic Setup
16
+
17
+ Configure the plugin with your Firebase service account credentials:
18
+
19
+ ```typescript
20
+ import { FlinkApp, FlinkContext } from "@flink-app/flink";
18
21
  import { firebaseMessagingPlugin } from "@flink-app/firebase-messaging-plugin";
19
22
 
20
23
  function start() {
21
24
  new FlinkApp<AppContext>({
22
25
  name: "My app",
23
26
  plugins: [
24
- firebaseMessagingPlugin({
25
- serverKey: "YOUR FIREBASE SERVER KEY"
26
- })
27
+ firebaseMessagingPlugin({
28
+ serviceAccountKey: process.env.FIREBASE_SERVICE_ACCOUNT_KEY_BASE64!,
29
+ exposeEndpoints: false // Optional: expose POST /send-message endpoint
30
+ })
27
31
  ],
28
32
  }).start();
29
33
  }
30
-
31
34
  ```
32
35
 
33
- Add it to your app context (normally `Ctx.ts` in the root folder of your project)
36
+ **Plugin Options:**
34
37
 
38
+ ```typescript
39
+ interface FirebaseMessagingPluginOptions {
40
+ serviceAccountKey: string; // Base64-encoded Firebase service account JSON
41
+ exposeEndpoints?: boolean; // Optional: expose HTTP endpoint (default: undefined)
42
+ permissions?: {
43
+ send: string; // Optional: permission required for endpoint
44
+ };
45
+ }
35
46
  ```
47
+
48
+ ### Getting Your Service Account Key
49
+
50
+ 1. Go to Firebase Console > Project Settings > Service Accounts
51
+ 2. Click "Generate New Private Key"
52
+ 3. Download the JSON file
53
+ 4. Base64 encode the JSON file contents:
54
+ ```bash
55
+ cat service-account.json | base64
56
+ ```
57
+ 5. Store the base64 string in your environment variable
58
+
59
+ ## TypeScript Setup
60
+
61
+ Add the plugin context to your app's context type (usually in `Ctx.ts`):
62
+
63
+ ```typescript
64
+ import { FlinkContext } from "@flink-app/flink";
36
65
  import { FirebaseMessagingContext } from "@flink-app/firebase-messaging-plugin";
37
66
 
38
67
  export interface Ctx extends FlinkContext<FirebaseMessagingContext> {
39
- ....
68
+ // Your other context properties
40
69
  }
70
+ ```
71
+
72
+ **Plugin Context Interface:**
41
73
 
74
+ ```typescript
75
+ interface FirebaseMessagingContext {
76
+ firebaseMessaging: {
77
+ send: (message: Message) => void;
78
+ };
79
+ }
42
80
  ```
43
81
 
44
- ## Configuration
82
+ ## Usage in Handlers
83
+
84
+ ### Basic Notification
45
85
 
46
- - `serverKey` - The firebase server key
86
+ Send a simple push notification to a single device:
47
87
 
88
+ ```typescript
89
+ import { Handler } from "@flink-app/flink";
90
+ import { Ctx } from "../Ctx";
91
+
92
+ const SendNotification: Handler<Ctx, any, any> = async ({ ctx, req }) => {
93
+ await ctx.plugins.firebaseMessaging.send({
94
+ to: ["device_token_here"],
95
+ notification: {
96
+ title: "New Message",
97
+ body: "You have received a new message"
98
+ },
99
+ data: {}
100
+ });
101
+
102
+ return { data: { success: true } };
103
+ };
104
+
105
+ export default SendNotification;
106
+ ```
107
+
108
+ ### Notification to Multiple Devices
109
+
110
+ Send the same notification to multiple devices:
111
+
112
+ ```typescript
113
+ await ctx.plugins.firebaseMessaging.send({
114
+ to: [
115
+ "device_token_1",
116
+ "device_token_2",
117
+ "device_token_3"
118
+ ],
119
+ notification: {
120
+ title: "System Update",
121
+ body: "A new version is available"
122
+ },
123
+ data: {
124
+ version: "2.0.0",
125
+ updateUrl: "https://example.com/update"
126
+ }
127
+ });
128
+ ```
129
+
130
+ ### Data-Only Message
131
+
132
+ Send a data message without notification (silent push):
133
+
134
+ ```typescript
135
+ await ctx.plugins.firebaseMessaging.send({
136
+ to: ["device_token"],
137
+ data: {
138
+ type: "sync",
139
+ timestamp: Date.now().toString(),
140
+ action: "refresh_data"
141
+ }
142
+ });
143
+ ```
144
+
145
+ ### Rich Notification with Custom Data
146
+
147
+ ```typescript
148
+ const SendOrderUpdate: Handler<Ctx, any, any> = async ({ ctx, req }) => {
149
+ const { userId, orderId, status } = req.body;
150
+
151
+ // Get user's device tokens from database
152
+ const user = await ctx.repos.userRepo.findById(userId);
153
+ const deviceTokens = user.pushNotificationTokens || [];
154
+
155
+ await ctx.plugins.firebaseMessaging.send({
156
+ to: deviceTokens,
157
+ notification: {
158
+ title: "Order Update",
159
+ body: `Your order #${orderId} is now ${status}`
160
+ },
161
+ data: {
162
+ orderId: orderId,
163
+ status: status,
164
+ screen: "order_details",
165
+ timestamp: Date.now().toString()
166
+ }
167
+ });
168
+
169
+ return { data: { sent: true } };
170
+ };
171
+ ```
48
172
 
49
- ## Use as a managementmodule in the management-api-plugin
173
+ ## API Reference
174
+
175
+ ### Message Type
176
+
177
+ ```typescript
178
+ interface Message {
179
+ to: string[]; // Array of FCM device tokens
180
+
181
+ notification?: {
182
+ title?: string; // Notification title
183
+ body?: string; // Notification body text
184
+ };
185
+
186
+ data: { [key: string]: string }; // Custom data payload (all values must be strings)
187
+ }
188
+ ```
189
+
190
+ **Important Notes:**
191
+ - The `data` field is required (can be an empty object `{}`)
192
+ - All values in the `data` object must be strings
193
+ - `notification` is optional - omit it for silent data messages
194
+ - The `send` method processes messages in batches of 500 devices
195
+
196
+ ### Context API
197
+
198
+ ```typescript
199
+ // Send a message
200
+ ctx.plugins.firebaseMessaging.send(message: Message): void
201
+ ```
202
+
203
+ The `send` method:
204
+ - Processes devices in batches of 500 (FCM limitation)
205
+ - Logs success/failure for each device to debug logs
206
+ - Handles errors gracefully without throwing
207
+
208
+ ## Registered Endpoints
209
+
210
+ If `exposeEndpoints` is enabled, the plugin registers:
211
+
212
+ ### POST /send-message
213
+
214
+ Send a push notification via HTTP endpoint.
215
+
216
+ **Request Body:**
217
+ ```typescript
218
+ {
219
+ to: string[]; // Device tokens
220
+ notification?: {
221
+ title?: string;
222
+ body?: string;
223
+ };
224
+ data: { [key: string]: string };
225
+ }
226
+ ```
227
+
228
+ **Response:**
229
+ ```typescript
230
+ {
231
+ data: {
232
+ failedDevices: string[] // Currently returns empty array
233
+ }
234
+ }
235
+ ```
236
+
237
+ **Example:**
238
+ ```bash
239
+ curl -X POST http://localhost:3000/send-message \
240
+ -H "Content-Type: application/json" \
241
+ -d '{
242
+ "to": ["device_token_123"],
243
+ "notification": {
244
+ "title": "Hello",
245
+ "body": "World"
246
+ },
247
+ "data": {}
248
+ }'
249
+ ```
250
+
251
+ **Permissions:**
252
+ - Default permission: `firebase-messaging:send`
253
+ - Can be customized via `permissions.send` option
254
+
255
+ ## Complete Example
256
+
257
+ Here's a complete example showing different notification scenarios:
258
+
259
+ ```typescript
260
+ import { Handler } from "@flink-app/flink";
261
+ import { Ctx } from "../Ctx";
262
+
263
+ interface NotificationRequest {
264
+ userIds: string[];
265
+ type: "message" | "order" | "reminder" | "alert";
266
+ title: string;
267
+ body: string;
268
+ data?: Record<string, any>;
269
+ }
270
+
271
+ const SendPushNotification: Handler<Ctx, NotificationRequest, { sent: number }> = async ({
272
+ ctx,
273
+ req
274
+ }) => {
275
+ const { userIds, type, title, body, data = {} } = req.body;
276
+
277
+ // Collect device tokens from all users
278
+ const users = await ctx.repos.userRepo.findByIds(userIds);
279
+ const deviceTokens: string[] = [];
280
+
281
+ for (const user of users) {
282
+ if (user.pushTokens && user.pushTokens.length > 0) {
283
+ deviceTokens.push(...user.pushTokens);
284
+ }
285
+ }
286
+
287
+ if (deviceTokens.length === 0) {
288
+ return { data: { sent: 0 } };
289
+ }
290
+
291
+ // Convert data values to strings (FCM requirement)
292
+ const stringData: { [key: string]: string } = {};
293
+ for (const [key, value] of Object.entries(data)) {
294
+ stringData[key] = String(value);
295
+ }
296
+
297
+ // Add metadata
298
+ stringData.type = type;
299
+ stringData.sentAt = new Date().toISOString();
300
+
301
+ // Send notification
302
+ await ctx.plugins.firebaseMessaging.send({
303
+ to: deviceTokens,
304
+ notification: {
305
+ title,
306
+ body
307
+ },
308
+ data: stringData
309
+ });
50
310
 
51
- Initiate the module and configure it:
311
+ return { data: { sent: deviceTokens.length } };
312
+ };
52
313
 
314
+ export default SendPushNotification;
53
315
  ```
54
- import { GetManagementModule as GetNotificationManagementModule } from "@flink-app/firebase-messaging-plugin"
55
- const notificationManagementModule = GetNotificationManagementModule({
316
+
317
+ ## Management API Integration
318
+
319
+ The plugin can be integrated with the Management API for advanced notification management:
320
+
321
+ ### Setup Management Module
322
+
323
+ ```typescript
324
+ import {
325
+ firebaseMessagingPlugin,
326
+ GetManagementModule
327
+ } from "@flink-app/firebase-messaging-plugin";
328
+ import { managementApiPlugin } from "@flink-app/management-api-plugin";
329
+
330
+ // Define user segments for targeting
331
+ const notificationManagementModule = GetManagementModule({
56
332
  ui: true,
57
333
  uiSettings: {
58
- title: "Notifications"
334
+ title: "Push Notifications"
59
335
  },
60
- segments : [{
61
- id : "all",
62
- description : "All app users",
63
- handler : async (ctx : Ctx) => {
64
- const users = await ctx.repos.userRepo.findAll({})
65
- return users.map(u=>({
66
- userId : u._id.toString(),
67
- pushToken : u.pushNotificationTokens.map(p=>p.token)
68
- }))
336
+ segments: [
337
+ {
338
+ id: "all",
339
+ description: "All users",
340
+ handler: async (ctx: Ctx) => {
341
+ const users = await ctx.repos.userRepo.findAll({});
342
+ return users.map(u => ({
343
+ userId: u._id.toString(),
344
+ pushToken: u.pushNotificationTokens || []
345
+ }));
346
+ }
69
347
  },
70
-
71
- }],
72
- data : [],
73
- })
348
+ {
349
+ id: "active",
350
+ description: "Active users (last 7 days)",
351
+ handler: async (ctx: Ctx) => {
352
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
353
+ const users = await ctx.repos.userRepo.findAll({
354
+ lastActiveAt: { $gte: sevenDaysAgo }
355
+ });
356
+ return users.map(u => ({
357
+ userId: u._id.toString(),
358
+ pushToken: u.pushNotificationTokens || []
359
+ }));
360
+ }
361
+ }
362
+ ],
363
+ data: [
364
+ {
365
+ id: "messageType",
366
+ description: "Message Type",
367
+ options: ["announcement", "update", "reminder", "alert"]
368
+ }
369
+ ],
370
+ messageSentCallback: async (ctx, targets, message) => {
371
+ // Log notification send event
372
+ console.log(`Sent notification to ${targets.length} users`);
373
+
374
+ // Store in database
375
+ await ctx.repos.notificationLogRepo.create({
376
+ sentAt: new Date(),
377
+ recipientCount: targets.length,
378
+ message: message
379
+ });
380
+ }
381
+ });
382
+
383
+ function start() {
384
+ new FlinkApp<Ctx>({
385
+ name: "My app",
386
+ plugins: [
387
+ firebaseMessagingPlugin({
388
+ serviceAccountKey: process.env.FIREBASE_SERVICE_ACCOUNT_KEY!
389
+ }),
390
+ managementApiPlugin({
391
+ token: process.env.MGMT_TOKEN!,
392
+ jwtSecret: process.env.JWT_SECRET!,
393
+ modules: [notificationManagementModule]
394
+ })
395
+ ]
396
+ }).start();
397
+ }
398
+ ```
399
+
400
+ ### Management Module Types
401
+
402
+ ```typescript
403
+ interface GetManagementModuleConfig {
404
+ pluginId?: string; // Default: "managementNotificationsApi"
405
+ ui: boolean; // Enable UI in admin panel
406
+ uiSettings?: {
407
+ title: string; // Module title in UI
408
+ };
409
+ segments: MessagingSegment[]; // User segments for targeting
410
+ data?: MessagingData[]; // Additional data fields
411
+ messageSentCallback?: ( // Callback after sending
412
+ ctx: any,
413
+ targets: MessagingTarget[],
414
+ message: Message
415
+ ) => void;
416
+ }
417
+
418
+ interface MessagingSegment {
419
+ id: string;
420
+ description: string;
421
+ handler: (ctx: FlinkContext<any>) => Promise<MessagingTarget[]>;
422
+ }
423
+
424
+ interface MessagingTarget {
425
+ userId: string;
426
+ pushToken: string[];
427
+ }
428
+
429
+ interface MessagingData {
430
+ id: string;
431
+ description: string;
432
+ options?: string[];
433
+ }
434
+ ```
435
+
436
+ ### Management Endpoints
437
+
438
+ When using the Management Module, these endpoints are registered:
439
+
440
+ #### GET /managementapi/{pluginId}
441
+ Get module information including segments and data fields.
442
+
443
+ #### POST /managementapi/{pluginId}/message
444
+ Send notification to a segment.
445
+
446
+ **Request:**
447
+ ```typescript
448
+ {
449
+ segmentId: string;
450
+ notification: {
451
+ title: string;
452
+ body: string;
453
+ };
454
+ data: { [key: string]: string };
455
+ }
456
+ ```
457
+
458
+ ## Best Practices
459
+
460
+ ### Device Token Management
461
+
462
+ Store and manage device tokens properly:
463
+
464
+ ```typescript
465
+ // When user logs in or registers device
466
+ const RegisterDeviceToken: Handler<Ctx, any, any> = async ({ ctx, req }) => {
467
+ const { userId, deviceToken } = req.body;
468
+
469
+ await ctx.repos.userRepo.update(userId, {
470
+ $addToSet: { pushTokens: deviceToken } // Avoid duplicates
471
+ });
472
+
473
+ return { data: { success: true } };
474
+ };
475
+
476
+ // When user logs out
477
+ const RemoveDeviceToken: Handler<Ctx, any, any> = async ({ ctx, req }) => {
478
+ const { userId, deviceToken } = req.body;
479
+
480
+ await ctx.repos.userRepo.update(userId, {
481
+ $pull: { pushTokens: deviceToken }
482
+ });
483
+
484
+ return { data: { success: true } };
485
+ };
74
486
  ```
75
487
 
488
+ ### Data Payload Conversion
76
489
 
490
+ Always convert data values to strings:
77
491
 
492
+ ```typescript
493
+ // Bad - will fail
494
+ data: {
495
+ userId: 123, // Number
496
+ isActive: true, // Boolean
497
+ metadata: { foo: "bar" } // Object
498
+ }
499
+
500
+ // Good - all strings
501
+ data: {
502
+ userId: "123",
503
+ isActive: "true",
504
+ metadata: JSON.stringify({ foo: "bar" })
505
+ }
506
+ ```
507
+
508
+ ### Notification vs Data Messages
509
+
510
+ **Notification Messages:**
511
+ - Display a notification in the system tray
512
+ - Wake up the app when tapped
513
+ - Best for user-facing alerts
514
+
515
+ **Data Messages:**
516
+ - Silent delivery to app
517
+ - App handles processing
518
+ - Best for background sync, state updates
519
+
520
+ ```typescript
521
+ // Notification message (user sees it)
522
+ {
523
+ notification: { title: "New message", body: "John sent you a message" },
524
+ data: { chatId: "123" }
525
+ }
526
+
527
+ // Data message (silent)
528
+ {
529
+ data: { type: "sync", lastSyncTimestamp: "1234567890" }
530
+ }
531
+ ```
532
+
533
+ ### Batch Processing
534
+
535
+ The plugin automatically handles batching (500 devices per batch), but you should still consider:
536
+
537
+ ```typescript
538
+ // Good - let the plugin handle batching
539
+ await ctx.plugins.firebaseMessaging.send({
540
+ to: thousandsOfTokens, // Plugin splits into batches of 500
541
+ notification: { title: "Update", body: "New feature available" },
542
+ data: {}
543
+ });
544
+
545
+ // Also good - send to specific segments
546
+ const activeUsers = await ctx.repos.userRepo.findActive();
547
+ const tokens = activeUsers.flatMap(u => u.pushTokens || []);
548
+ await ctx.plugins.firebaseMessaging.send({
549
+ to: tokens,
550
+ notification: { title: "Hello active users!", body: "..." },
551
+ data: {}
552
+ });
553
+ ```
554
+
555
+ ### Error Handling
556
+
557
+ The plugin logs errors internally but doesn't throw. Monitor logs:
558
+
559
+ ```typescript
560
+ // The plugin logs:
561
+ // - Success: "[firebaseMessaging] Successfully sent to device {token}"
562
+ // - Failure: "[firebaseMessaging] Failed sending to device {token}: {error}"
563
+ // - Batch error: "[firebaseMessaging] Failed sending batch: {error}"
564
+
565
+ // You can wrap sends with try-catch if needed
566
+ try {
567
+ await ctx.plugins.firebaseMessaging.send({
568
+ to: deviceTokens,
569
+ notification: { title: "Test", body: "Test" },
570
+ data: {}
571
+ });
572
+ } catch (error) {
573
+ console.error("Unexpected error:", error);
574
+ }
575
+ ```
576
+
577
+ ## Troubleshooting
578
+
579
+ ### Notifications Not Received
580
+
581
+ 1. **Invalid device tokens** - Tokens expire or become invalid
582
+ - Implement token refresh on the client side
583
+ - Remove invalid tokens from your database
584
+
585
+ 2. **Service account permissions** - Verify the service account has FCM permissions
586
+ - Check Firebase Console > Project Settings > Service Accounts
587
+ - Ensure "Firebase Admin SDK" role is granted
588
+
589
+ 3. **Base64 encoding** - Ensure service account key is properly encoded
590
+ ```bash
591
+ # Verify decoding works
592
+ echo $FIREBASE_SERVICE_ACCOUNT_KEY_BASE64 | base64 -d
593
+ ```
594
+
595
+ 4. **App not running** - Data messages require the app to be running
596
+ - Use notification messages to wake the app
597
+ - Combine notification + data for best results
598
+
599
+ ### Invalid Data Format
600
+
601
+ Error: "All data values must be strings"
602
+
603
+ ```typescript
604
+ // Fix: Convert all values to strings
605
+ data: {
606
+ count: String(42),
607
+ timestamp: new Date().toISOString(),
608
+ metadata: JSON.stringify({ key: "value" })
609
+ }
610
+ ```
611
+
612
+ ### Check Debug Logs
613
+
614
+ Enable Flink debug mode to see detailed FCM logs:
615
+
616
+ ```typescript
617
+ new FlinkApp<Ctx>({
618
+ name: "My app",
619
+ debug: true, // Enable debug logging
620
+ plugins: [
621
+ firebaseMessagingPlugin({ ... })
622
+ ]
623
+ }).start();
624
+ ```
78
625
 
626
+ ## Notes
79
627
 
628
+ - Messages are processed in batches of 500 devices (FCM limitation)
629
+ - All data values must be strings (numbers, booleans, objects must be converted)
630
+ - The plugin uses Firebase Admin SDK v11+
631
+ - Device tokens should be stored securely and updated regularly
632
+ - Invalid/expired tokens are logged but don't stop delivery to other devices
633
+ - The `send` method is fire-and-forget (doesn't wait for delivery confirmation)