Skip to content

Repository files navigation

Notification Service

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.

Request flow

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.

Features

  • 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 notificationId in the data payload for client-side de-duplication
  • Optional RabbitMQ consumer with bounded retries and a .dead queue
  • Liveness/readiness endpoints and graceful shutdown
  • No Firebase credentials stored in source control

1. Firebase Spark setup

  1. Create a project in the Firebase console and keep it on the Spark plan.

  2. Open Build -> Firestore Database, create a database, and choose a region close to your users.

  3. 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.

  4. Open Project settings -> Service accounts -> Generate new private key. Download the JSON once and keep it outside the repository.

  5. Copy its project_id, client_email, and private_key values into .env as shown in .env.example. Preserve \n inside the private key.

  6. Add the mobile application in Firebase project settings:

    • Android: add google-services.json to 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.

Do not commit the downloaded service-account JSON. The .gitignore also blocks common service-account filenames as an extra safeguard.

2. Local development

Node.js 22 or newer is required.

npm install
cp .env.example .env
npm run dev

On 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 start

The 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.

Environment variables

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.

HTTP API

Health

GET /health
GET /ready

/health reports whether the process is alive. /ready reports whether Firebase was initialized and, when enabled, RabbitMQ is connected.

Register a mobile device

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.

Unregister a mobile device

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"
}

Send to a user

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.

RabbitMQ event

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.

Production notes

  • Store credentials in the deployment platform's secret manager rather than a checked-in .env file.
  • 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 notificationId to 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages