Перейти к содержанию

Notifications Service — Technical Reference

Overview

Redis Streams consumer that delivers push notifications to mobile devices via Firebase Cloud Messaging (FCM). It reads jobs from a Redis Stream, looks up device tokens from PostgreSQL, sends notifications via Firebase Admin SDK, and logs delivery status.

Delivery semantics: best-effort (at-most-once from the user perspective). The service does not guarantee delivery if the device is offline or the token is stale. Failed messages are retried up to 5 times via XAUTOCLAIM before being discarded.

Architecture

Stream Flow

Technology Stack

Component Technology
Runtime Node.js 20+
Stream consumer ioredis (XREADGROUP / XAUTOCLAIM)
Push delivery firebase-admin SDK
Database pg (PostgreSQL)
Logging pino (JSON structured logs)

Process Model

Single Node.js process:

  1. Entry point (src/index.js): validates required env vars, initialises Firebase, starts the consumer loop, registers SIGTERM/SIGINT handlers for graceful shutdown
  2. Consumer loop (src/consumer.js): XREADGROUP blocking read + periodic XAUTOCLAIM for idle messages
  3. Job handler (src/handler.js): processes one message — token lookup, FCM send, DB log update

Processing Flow

For each message received from stream.notifications.jobs:

  1. Parse payload — message fields are a flat key/value list (Redis XADD format) or a single payload field containing JSON
  2. Check delivery count — if >= maxDeliveryCount (5), acknowledge and discard (logged as notifications.max_retries)
  3. Insert log entry — creates a row in notifications.notification_log with status='pending'
  4. Fetch device tokens — queries public.device_token WHERE "userId" = $1
  5. Send via FCM — calls sendOne() for each token individually (one FCM request per token)
  6. Handle errors per token:
  7. action='remove'DELETE FROM public.device_token WHERE id = $1 (stale token cleanup)
  8. action='fatal' → logs notifications.fcm_fatal alert (APNs/auth config issue)
  9. action='retry' → no XACK — message reclaimed via XAUTOCLAIM after idle threshold
  10. Update log entry — sets final status: sent | partial | failed | no_tokens
  11. XACK — acknowledges the message on success (or on permanent failure: no tokens, max retries)

FCM Error Classification

FCM error code Action Description
messaging/invalid-registration-token remove Token is invalid — delete from DB
messaging/registration-token-not-registered remove App uninstalled — delete token
messaging/invalid-argument remove Malformed token
messaging/mismatched-credential remove Token belongs to different project
messaging/server-unavailable retry FCM temporary error
messaging/unknown-error retry Transient error
messaging/internal-error retry FCM internal
messaging/quota-exceeded retry Rate limited
messaging/third-party-auth-error fatal APNs auth failure — requires operator action
messaging/authentication-error fatal FCM credential problem
(unknown) retry Safe default

Retry / XAUTOCLAIM

Messages that are not XACK'd remain pending in the consumer group. A setInterval running every AUTOCLAIM_MIN_IDLE_MS (default 30s) calls XAUTOCLAIM to reclaim messages that have been idle in the pending list. After MAX_DELIVERY_COUNT (default 5) deliveries, the message is XACK'd and discarded (logged as notifications.max_retries).

FCM Message Format

Messages are sent to Android, iOS, and web simultaneously. Each sendOne() call builds a message with:

  • notification: { title, body } — visible notification text
  • data: all data fields from payload, plus channel, all stringified
  • android: high priority, channelId set to channel
  • apns: priority 10, aps.badge (if provided), mutable-content: 1
  • webpush: high urgency header

Graceful Shutdown

On SIGTERM or SIGINT: 1. Sets shuttingDown = true — consumer loop exits after the current iteration 2. Calls redis.quit() — closes Redis connection 3. Calls pool.end() — closes PostgreSQL pool 4. process.exit(0)

Key Files

File Purpose
src/index.js Entry point — startup, shutdown, orchestration
src/consumer.js XREADGROUP loop + XAUTOCLAIM retry
src/handler.js Job processing: token lookup, FCM send, DB log
src/fcm.js Firebase Admin SDK init + sendOne() + error classification
src/config.js Env var loading + validateOrExit()
src/db.js PostgreSQL pool singleton
src/redis.js ioredis singleton
src/log.js pino logger
sql/001_init.sql Creates notifications schema and notification_log table