Firebase Cloud Messaging (FCM) notification microservice for SynGo-D. It
registers a user's mobile device tokens, sends a notification to every device
belonging to a user, removes expired tokens, and can optionally consume the
platform's existing RabbitMQ notification_queue.
FCM itself is available on Firebase's free Spark plan. This service stores device tokens in Cloud Firestore, so a small project can also stay inside Firestore's Spark free quota. Monitor the Firebase usage dashboard as the application grows.
Mobile app --session JWT--> POST /api/devices
|
v
Firestore
Backend --internal token--> POST /api/notifications --Firebase Admin SDK--> FCM
RabbitMQ notification_queue --------------------------^ |
v
User devices
The service account is used only by this backend. Never include a Firebase
service-account private key or INTERNAL_SERVICE_TOKEN in the mobile app.
- Android, iOS, and web FCM token registration
- User identity taken from a verified session JWT, never from the request body
- Internal send endpoint protected with a timing-safe shared-token comparison
- Delivery to all of a user's registered devices
- Automatic FCM batching at 500 tokens per request
- Retry of transient per-device FCM errors
- Automatic removal of expired/unregistered tokens
- Stable
notificationIdin the data payload for client-side de-duplication - Optional RabbitMQ consumer with bounded retries and a
.deadqueue - Liveness/readiness endpoints and graceful shutdown
- No Firebase credentials stored in source control
-
Create a project in the Firebase console and keep it on the Spark plan.
-
Open Build -> Firestore Database, create a database, and choose a region close to your users.
-
Because this backend uses the Admin SDK, mobile clients do not need direct access to the token collection. A safe starting Firestore rule is:
match /device_tokens/{document=**} { allow read, write: if false; }Admin SDK requests use service-account authorization and are not controlled by client Firestore rules.
-
Open Project settings -> Service accounts -> Generate new private key. Download the JSON once and keep it outside the repository.
-
Copy its
project_id,client_email, andprivate_keyvalues into.envas shown in.env.example. Preserve\ninside the private key. -
Add the mobile application in Firebase project settings:
- Android: add
google-services.jsonto the Android app and install the FCM client SDK. - iOS: add
GoogleService-Info.plist, install the FCM client SDK, and upload an APNs authentication key in Firebase. Apple developer-program costs are separate from Firebase.
- Android: add
Do not commit the downloaded service-account JSON. The .gitignore also blocks
common service-account filenames as an extra safeguard.
Node.js 22 or newer is required.
npm install
cp .env.example .env
npm run devOn Windows PowerShell, use Copy-Item .env.example .env instead of cp if
needed. Set JWT_SECRET to the same value used by main-backend. Generate a
separate internal token, for example:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Useful commands:
npm test
npm run build
npm startThe server verifies its Firebase credential during startup. A missing, revoked, or incorrectly copied private key therefore stops the process immediately instead of failing only when the first notification is sent.
| Variable | Required | Purpose |
|---|---|---|
PORT |
No | HTTP port; default 5003 |
JWT_SECRET |
Yes | Verifies user sessions issued by main-backend |
INTERNAL_SERVICE_TOKEN |
Yes | Authenticates trusted services sending notifications |
FIREBASE_PROJECT_ID |
Local | Firebase service-account project ID |
FIREBASE_CLIENT_EMAIL |
Local | Firebase service-account client email |
FIREBASE_PRIVATE_KEY |
Local | Firebase service-account private key |
FIRESTORE_DEVICE_TOKENS_COLLECTION |
No | Token collection; default device_tokens |
RABBITMQ_ENABLED |
No | Enables queue consumption; default false |
RABBITMQ_URL |
If enabled | Shared RabbitMQ connection URL |
RABBITMQ_NOTIFICATION_QUEUE |
No | Queue name; default notification_queue |
RABBITMQ_PREFETCH |
No | Concurrent unacknowledged deliveries; default 10 |
RABBITMQ_MAX_RETRIES |
No | Broker retries before dead-lettering; default 3 |
In Google Cloud environments, the three explicit Firebase credential values can be omitted and Application Default Credentials can be used instead.
GET /health
GET /ready/health reports whether the process is alive. /ready reports whether
Firebase was initialized and, when enabled, RabbitMQ is connected.
The mobile app first gets an FCM token from the Firebase client SDK, then sends it with the normal SynGo-D session JWT:
POST /api/devices
Authorization: Bearer <user-session-jwt>
Content-Type: application/json
{
"token": "fcm-registration-token",
"platform": "android"
}platform can be android, ios, or web. Call this endpoint after login and
whenever the Firebase client SDK refreshes the token.
Call this during logout before discarding the local session:
DELETE /api/devices
Authorization: Bearer <user-session-jwt>
Content-Type: application/json
{
"token": "fcm-registration-token"
}Only a trusted backend service should call this endpoint:
POST /api/notifications
X-Internal-Service-Token: <internal-service-token>
Content-Type: application/json
{
"userId": "a-user-id",
"title": "Analysis complete",
"body": "Your pull request report is ready.",
"notificationId": "analysis-42-completed",
"data": {
"screen": "analysis/42",
"pullRequest": "42"
}
}FCM requires every custom data value to be a string. notificationId is
optional; the service generates one when it is omitted.
Example response:
{
"success": true,
"notificationId": "analysis-42-completed",
"targeted": 2,
"sent": 2,
"failed": 0,
"removedInvalidTokens": 0,
"errorCounts": {}
}No registered devices is a successful no-op with targeted: 0.
Set RABBITMQ_ENABLED=true to consume the durable notification_queue. Each
message uses the same JSON shape as POST /api/notifications. Invalid messages
and messages that exhaust their retries are moved to
notification_queue.dead rather than retried forever.
Publisher example:
{
"userId": "a-user-id",
"title": "Analysis complete",
"body": "Your pull request report is ready.",
"notificationId": "analysis-42-completed",
"data": {
"screen": "analysis/42"
}
}The queue is declared without custom arguments so it remains compatible with the existing SynGo-D RabbitMQ repository.
- Store credentials in the deployment platform's secret manager rather than a
checked-in
.envfile. - Use one Firebase project for development and another for production.
- Set budget alerts and monitor Firestore reads/writes even while using Spark.
- Treat notification delivery as at-least-once. Mobile clients should use
notificationIdto avoid showing duplicates after an ambiguous network retry. - FCM acceptance does not guarantee the person saw the notification. FCM does not provide a reliable "read" receipt; implement an app-side acknowledgement if the product needs that information.
The full local install may currently show moderate advisories in Firebase
Admin's optional Cloud Storage dependency tree. This service does not use Cloud
Storage, and the production image omits optional dependencies while declaring
Firestore directly. npm audit --omit=dev --omit=optional checks the dependency
set that is actually shipped.