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¶

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:
- Entry point (
src/index.js): validates required env vars, initialises Firebase, starts the consumer loop, registers SIGTERM/SIGINT handlers for graceful shutdown - Consumer loop (
src/consumer.js):XREADGROUPblocking read + periodicXAUTOCLAIMfor idle messages - 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:
- Parse payload — message fields are a flat key/value list (Redis XADD format) or a single
payloadfield containing JSON - Check delivery count — if
>= maxDeliveryCount(5), acknowledge and discard (logged asnotifications.max_retries) - Insert log entry — creates a row in
notifications.notification_logwithstatus='pending' - Fetch device tokens — queries
public.device_token WHERE "userId" = $1 - Send via FCM — calls
sendOne()for each token individually (one FCM request per token) - Handle errors per token:
action='remove'→DELETE FROM public.device_token WHERE id = $1(stale token cleanup)action='fatal'→ logsnotifications.fcm_fatalalert (APNs/auth config issue)action='retry'→ no XACK — message reclaimed via XAUTOCLAIM after idle threshold- Update log entry — sets final status:
sent|partial|failed|no_tokens - 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 textdata: alldatafields from payload, pluschannel, all stringifiedandroid: high priority,channelIdset tochannelapns: priority 10,aps.badge(if provided),mutable-content: 1webpush: 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 |