Scalable Push Notifications Architecture: A Language-Agnostic Approach

Scalable Push Notifications Architecture: A Language-Agnostic Approach

Introduction

Push notifications are essential for user engagement, but most implementations only scratch the surface. They focus on sending messages without addressing token lifecycle management, delivery tracking, notification history, or scalability concerns.

This post presents a comprehensive architecture for push notifications that works regardless of your tech stack. Whether you're using Flutter/React Native/Swift on mobile, or Node.js/Python/Java/Go on the backend, these concepts apply universally.

What makes this architecture different:

  • Automatic token synchronization between client and server
  • Complete notification history with CRUD operations
  • Delivery tracking and error handling
  • Multi-device support per user
  • Built-in retry mechanisms
  • Scalable from day one

The Challenge

Implementing a robust push notification system involves several interconnected challenges:

1. Token Lifecycle Management

  • Tokens expire and need refreshing
  • Users have multiple devices (phone, tablet, web)
  • Tokens become invalid (app uninstall, permissions revoked)
  • Need to sync token state between client and server

2. Delivery Reliability

  • Network failures during send
  • Push service rate limits (FCM/APNs)
  • Invalid tokens need identification
  • Failed deliveries require retry logic

3. User Experience

  • View notification history
  • Mark notifications as read/unread
  • Delete unwanted notifications
  • Receive notifications even when app is closed

4. Scalability

  • Send to thousands of users simultaneously
  • Handle burst traffic during campaigns
  • Manage growing notification history
  • Optimize database queries

5. Observability

  • Track delivery success/failure rates
  • Monitor push service errors
  • Measure user engagement (open rates)
  • Debug delivery issues

Architecture Overview

The solution follows a client-server pattern with asynchronous processing:

┌──────────────────────────────────────────────────────────┐
│                     MOBILE CLIENT                        │
│                                                          │
│  ┌─────────────────────────────────────────────┐         │
│  │  Push Service (FCM/APNs)                    │         │
│  │  • Request permissions                       │        │
│  │  • Obtain device token                       │        │
│  │  • Listen for token refresh                  │        │
│  │  • Handle incoming notifications             │        │
│  └─────────────────────────────────────────────┘         │
│                        │                                 │
│                        │ 1. Register/Update Token        │
└────────────────────────┼─────────────────────────────────┘
                         │
                         ▼
┌──────────────────────────────────────────────────────────┐
│                    API SERVER                            │
│                                                          │
│  ┌──────────────────────────────────────────┐            │
│  │  REST API Endpoints                      │            │
│  │  • POST   /api/tokens                    │            │
│  │  • DELETE /api/tokens/:id                │            │
│  │  • POST   /api/notifications             │            │
│  │  • GET    /api/notifications             │            │
│  │  • PATCH  /api/notifications/:id/read    │            │
│  │  • DELETE /api/notifications/:id         │            │
│  └──────────────────────────────────────────┘            │
│                        │                                 │
└────────────────────────┼─────────────────────────────────┘
                         │
                         ▼
┌──────────────────────────────────────────────────────────┐
│                     DATABASE                             │
│                                                          │
│  ┌─────────────────────┐    ┌─────────────────────┐      │
│  │  device_tokens      │    │  notifications      │      │
│  │  • user_id          │◄───│  • user_id          │      │
│  │  • token            │    │  • title            │      │
│  │  • platform         │    │  • body             │      │
│  │  • is_active        │    │  • data (JSON)      │      │
│  │  • last_used_at     │    │  • status           │      │
│  └─────────────────────┘    │  • sent_at          │      │
│                             │  • read_at          │      │
│                             └─────────────────────┘      │
│                                       │                  │
└───────────────────────────────────────┼──────────────────┘
                                        │
                                        │ 2. Trigger Async Send
                                        ▼
┌──────────────────────────────────────────────────────────┐
│               BACKGROUND WORKER                          │
│                                                          │
│  ┌───────────────────────────────────────────┐           │
│  │  Push Delivery Service                    │           │
│  │  • Fetch notification                     │           │
│  │  • Get active tokens for user             │           │
│  │  • Send via FCM/APNs                      │           │
│  │  • Handle responses                       │           │
│  │  • Invalidate bad tokens                  │           │
│  │  • Update notification status             │           │
│  │  • Log delivery results                   │           │
│  └───────────────────────────────────────────┘           │
│                                                          │
│  ┌───────────────────────────────────────────┐           │
│  │  Retry Worker                             │           │
│  │  • Find failed/pending notifications      │           │
│  │  • Retry with exponential backoff         │           │
│  │  • Max retry limit (e.g., 3 attempts)     │           │
│  └───────────────────────────────────────────┘           │
└──────────────────────────────────────────────────────────┘
                         │
                         │ 3. Send Push
                         ▼
┌──────────────────────────────────────────────────────────┐
│           PUSH SERVICE (FCM/APNs)                        │
└──────────────────────────────────────────────────────────┘

Core Components

  1. Client Token Manager: Monitors token lifecycle and syncs with server
  2. Token Registry API: Receives and stores device tokens
  3. Notifications CRUD API: Manages notification records
  4. Push Delivery Service: Sends notifications via FCM/APNs
  5. Background Worker: Handles retries and batch processing
  6. History UI: Shows notification history to users

Database Schema Design

Device Tokens Table

CREATE TABLE device_tokens (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token TEXT NOT NULL,
    platform VARCHAR(20) NOT NULL, -- 'ios', 'android', 'web'
    device_info JSONB, -- model, OS version, etc.
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_used_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- One user can have multiple devices
    UNIQUE(user_id, token)
);

-- Indexes for performance
CREATE INDEX idx_device_tokens_user_id ON device_tokens(user_id);
CREATE INDEX idx_device_tokens_active ON device_tokens(is_active);
CREATE INDEX idx_device_tokens_last_used ON device_tokens(last_used_at);

Key Design Decisions:

  • UNIQUE(user_id, token): Prevents duplicate tokens per user
  • is_active: Soft delete for audit trail
  • last_used_at: Identify stale tokens for cleanup
  • Support multiple devices per user

Notifications Table

CREATE TABLE notifications (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    data JSONB DEFAULT '{}', -- Custom payload for app navigation
    type VARCHAR(50), -- 'general', 'alert', 'promotion', etc.
    priority VARCHAR(20) DEFAULT 'normal', -- 'low', 'normal', 'high'
    status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'sent', 'failed', 'cancelled'
    sent_at TIMESTAMP WITH TIME ZONE,
    read_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- Delivery metadata
    fcm_message_id TEXT, -- ID from FCM/APNs response
    error_message TEXT,
    retry_count INTEGER DEFAULT 0
);

-- Critical indexes
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
CREATE INDEX idx_notifications_status ON notifications(status);
CREATE INDEX idx_notifications_created_at ON notifications(created_at DESC);
CREATE INDEX idx_notifications_user_status ON notifications(user_id, status);
CREATE INDEX idx_notifications_unread ON notifications(user_id, read_at) 
    WHERE read_at IS NULL;

Status Lifecycle:

pending → sent → (optionally) delivered
        ↓
      failed → retry (up to max attempts)
CREATE TABLE notification_deliveries (
    id UUID PRIMARY KEY,
    notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
    token_id UUID NOT NULL REFERENCES device_tokens(id) ON DELETE CASCADE,
    status VARCHAR(20) NOT NULL, -- 'success', 'failed'
    error_code VARCHAR(50),
    error_message TEXT,
    sent_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_deliveries_notification ON notification_deliveries(notification_id);
CREATE INDEX idx_deliveries_token ON notification_deliveries(token_id);

Purpose: Track delivery per device for debugging and analytics

API Design

Token Management Endpoints

POST /api/tokens - Register or Update Token

Purpose: Upsert device token for authenticated user
Auth: Required (JWT/Session)

Request Body:
{
  "token": "device_fcm_or_apns_token",
  "platform": "ios" | "android" | "web",
  "device_info": {
    "model": "iPhone 15 Pro",
    "os_version": "iOS 17.1",
    "app_version": "1.2.3"
  }
}

Response: 201 Created
{
  "success": true,
  "data": {
    "id": "uuid",
    "token": "...",
    "platform": "ios"
  }
}

Logic:
1. Extract user_id from auth token
2. Validate input (token, platform required)
3. UPSERT: 
   - If exists: Update metadata, set is_active=true
   - If new: Insert record
4. Update last_used_at timestamp

DELETE /api/tokens/:token - Deactivate Token

Purpose: Mark token as inactive (user logout)
Auth: Required

Logic:
- Set is_active = false
- Keep record for audit (soft delete)
- Don't physically delete

Notifications CRUD Endpoints

POST /api/notifications - Create Notification

Purpose: Create notification + trigger async push delivery
Auth: Required (typically admin role)

Request Body:
{
  "user_id": "uuid",
  "title": "New message",
  "body": "You have a new message from John",
  "data": {
    "type": "message",
    "message_id": "123",
    "sender_id": "456"
  },
  "type": "message",
  "priority": "high"
}

Response: 201 Created
{
  "success": true,
  "data": {
    "id": "uuid",
    "status": "pending",
    "created_at": "2025-10-22T10:30:00Z"
  }
}

Flow:
1. Validate authorization (admin check)
2. Validate required fields
3. Insert notification record (status: 'pending')
4. Commit transaction
5. Return 201 immediately
6. Trigger async worker to send push (don't block!)

GET /api/notifications - List User Notifications

Purpose: Fetch notification history
Auth: Required

Query Params:
- page: number (default: 1)
- limit: number (default: 20, max: 100)
- status: 'sent' | 'failed' (optional filter)
- type: string (optional filter)

Response: 200 OK
{
  "success": true,
  "data": [...notifications],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "has_more": true
  }
}

Logic:
- Filter by authenticated user_id
- Apply status/type filters if provided
- Order by created_at DESC (newest first)
- Paginate results

GET /api/notifications/:id - Get Single Notification

Purpose: Fetch notification details
Auth: Required
Security: Verify notification belongs to user

PATCH /api/notifications/:id/read - Mark as Read

Purpose: Update read_at timestamp
Auth: Required

Logic:
- Set read_at = NOW() WHERE id = :id AND user_id = :user_id
- Only update if currently null (idempotent)
- Return updated notification

DELETE /api/notifications/:id - Delete Notification

Purpose: Remove from user's history
Auth: Required

Logic:
- Physical delete (or soft delete based on requirements)
- Verify ownership before delete

Client-Side Implementation

Token Management Flow

App Lifecycle → Token Events → Server Sync

1. App Startup:
   - Request notification permissions
   - Get current token
   - Send to server

2. Token Refresh (automatic):
   - Listen for refresh event
   - Get new token
   - Send to server

3. User Login:
   - Re-send token with user context
   - Associate token with user_id

4. User Logout:
   - Call DELETE /api/tokens/:token
   - Mark token as inactive

Pseudo-code Pattern

class PushNotificationManager {
  async initialize() {
    // Request permissions
    const hasPermission = await requestNotificationPermission();
    if (!hasPermission) return;
    
    // Get initial token
    const token = await getDeviceToken();
    await this.registerToken(token);
    
    // Listen for token refresh
    onTokenRefresh((newToken) => {
      this.registerToken(newToken);
    });
    
    // Setup message handlers
    this.setupMessageHandlers();
  }
  
  async registerToken(token) {
    const maxRetries = 3;
    let attempt = 0;
    
    while (attempt < maxRetries) {
      try {
        await httpClient.post('/api/tokens', {
          token: token,
          platform: this.getPlatform(),
          device_info: await this.getDeviceInfo()
        });
        return; // Success
      } catch (error) {
        attempt++;
        if (attempt >= maxRetries) {
          // Store locally and retry later
          await this.saveTokenToLocalStorage(token);
          return;
        }
        // Exponential backoff
        await sleep(Math.pow(2, attempt) * 1000);
      }
    }
  }
  
  setupMessageHandlers() {
    // When app is in foreground
    onMessageReceived((message) => {
      this.showLocalNotification(message);
      this.updateNotificationsList();
    });
    
    // When user taps notification
    onNotificationTapped((message) => {
      this.navigateToScreen(message.data);
    });
  }
  
  getPlatform() {
    // Return 'ios', 'android', or 'web'
  }
  
  async getDeviceInfo() {
    return {
      model: await getDeviceModel(),
      os_version: await getOSVersion(),
      app_version: getAppVersion()
    };
  }
}

Notification History UI Pattern

class NotificationsScreen {
  state = {
    notifications: [],
    page: 1,
    hasMore: true,
    isLoading: false
  }
  
  async loadNotifications() {
    if (this.state.isLoading || !this.state.hasMore) return;
    
    this.state.isLoading = true;
    
    try {
      const response = await httpClient.get(
        `/api/notifications?page=${this.state.page}&limit=20`
      );
      
      this.state.notifications.push(...response.data);
      this.state.page++;
      this.state.hasMore = response.pagination.has_more;
    } finally {
      this.state.isLoading = false;
    }
  }
  
  async markAsRead(notificationId) {
    await httpClient.patch(`/api/notifications/${notificationId}/read`);
    // Update local state
  }
  
  async deleteNotification(notificationId) {
    await httpClient.delete(`/api/notifications/${notificationId}`);
    // Remove from local list
  }
  
  // Infinite scroll trigger
  onScrollNearBottom() {
    this.loadNotifications();
  }
}

Server-Side Push Delivery

Push Delivery Service Logic

Function: sendPushNotification(notificationId)

1. Fetch notification from database
   - If not found → exit
   
2. Fetch all active tokens for user_id
   - Query: WHERE user_id = X AND is_active = true
   - If no tokens → mark notification as 'failed' with reason
   
3. Build push message payload:
   {
     notification: { title, body },
     data: { custom fields from notification.data },
     android: { priority, channel_id, sound },
     ios: { sound, badge_count }
   }
   
4. Send to FCM/APNs:
   - Use multicast API for multiple tokens
   - Handle rate limits (max 500 tokens per request)
   
5. Process each response:
   FOR EACH token_response:
     IF success:
       - Log success in delivery_logs
       - Update token.last_used_at
     ELSE:
       - Log failure with error_code
       - IF error indicates invalid token:
           - Set token.is_active = false
           
6. Update notification status:
   IF any_success:
     - status = 'sent'
     - sent_at = NOW()
   ELSE:
     - status = 'failed'
     - error_message = aggregated errors

Invalid Token Detection

Certain error codes indicate a token should be deactivated:

FCM Error Codes:

  • messaging/invalid-registration-token
  • messaging/registration-token-not-registered
  • messaging/invalid-argument

APNs Error Codes:

  • BadDeviceToken
  • Unregistered
  • DeviceTokenNotForTopic

Action: Automatically set is_active = false for these tokens

Platform-Specific Payloads

Android (FCM):

{
  "message": {
    "token": "device_token",
    "notification": {
      "title": "Title",
      "body": "Body text"
    },
    "data": {
      "custom_key": "custom_value"
    },
    "android": {
      "priority": "high",
      "notification": {
        "channel_id": "default",
        "sound": "default",
        "color": "#FF0000"
      }
    }
  }
}

iOS (APNs):

{
  "aps": {
    "alert": {
      "title": "Title",
      "body": "Body text"
    },
    "sound": "default",
    "badge": 5,
    "content-available": 1
  },
  "custom_key": "custom_value"
}

Background Worker

Retry Worker Strategy

Schedule: Every 5 minutes (cron/scheduler)

Query for retryable notifications:
  SELECT id FROM notifications
  WHERE status IN ('pending', 'failed')
    AND retry_count < 3
    AND created_at > NOW() - INTERVAL '24 hours'
    AND (sent_at IS NULL OR sent_at < NOW() - INTERVAL '5 minutes')
  ORDER BY created_at ASC
  LIMIT 100
  
Process:
  1. Increment retry_count for selected notifications
  2. FOR EACH notification:
       - Call sendPushNotification(id)
       - Catch and log errors
  3. Sleep before next iteration

Exponential Backoff:
  - Retry 1: Immediate
  - Retry 2: After 5 minutes
  - Retry 3: After 15 minutes
  - After 3 failures: Mark as 'failed' permanently

Implementation Options by Language

Node.js:

  • Libraries: node-cronbullagenda
  • Example: BullMQ for robust job queue

Python:

  • Libraries: celeryapschedulerrq
  • Example: Celery with Redis backend

Java:

  • Libraries: Quartz Scheduler, Spring @Scheduled
  • Example: Spring Boot scheduled tasks

Go:

  • Libraries: crongocron
  • Example: robfig/cron package

Best Practices

1. Security

Authorization Checks:

- Only admins can create notifications for any user
- Regular users can only query their own notifications
- Always verify user_id matches authenticated user
- Use role-based access control (RBAC)

Rate Limiting:

Apply rate limits to prevent abuse:
- Per user: 100 notifications per 15 minutes
- Per IP: 1000 requests per hour
- Per endpoint: Adjust based on sensitivity

Input Validation:

- Sanitize title and body (max lengths)
- Validate JSON in data field
- Check platform enum values
- Prevent SQL injection (use parameterized queries)

2. Performance Optimization

Database Indexing:

-- Critical for query performance
CREATE INDEX idx_notifications_user_created 
  ON notifications(user_id, created_at DESC);

CREATE INDEX idx_notifications_status_created 
  ON notifications(status, created_at) 
  WHERE status IN ('pending', 'failed');

CREATE INDEX idx_tokens_user_active 
  ON device_tokens(user_id, is_active) 
  WHERE is_active = true;

Caching Strategy:

Cache with TTL:
- Unread count: 5-15 minutes
- User preferences: 30 minutes
- Active tokens: 10 minutes

Invalidate on:
- New notification created
- Notification marked as read
- Token registered/deactivated

Pagination:

- Default limit: 20 items
- Max limit: 100 items
- Use cursor-based pagination for large datasets
- Return total count in metadata

3. Scalability

Horizontal Scaling:

API Servers:
- Stateless design (store sessions in Redis/DB)
- Load balancer distributes requests
- Auto-scale based on CPU/memory

Workers:
- Multiple worker processes
- Use distributed job queue (RabbitMQ, SQS, Kafka)
- Prevent duplicate processing with locks

Database Partitioning:

-- Partition notifications by month
CREATE TABLE notifications_2025_10 
  PARTITION OF notifications
  FOR VALUES FROM ('2025-10-01') TO ('2025-11-01');

-- Benefits:
-- - Faster queries on recent data
-- - Easy archival of old data
-- - Improved maintenance operations

Archival Strategy:

Move old notifications (> 90 days) to:
- Cold storage (AWS S3, Google Cloud Storage)
- Separate archive table
- Data warehouse for analytics

Keep recent data hot for fast access

4. Monitoring & Observability

Key Metrics to Track:

Delivery Metrics:
- Total notifications sent (by period)
- Success rate (sent / attempted)
- Failure rate + error distribution
- Average delivery time

Engagement Metrics:
- Read rate (read / sent)
- Open rate (tapped / sent)
- Conversion rate (action taken / sent)

System Metrics:
- API response times
- Worker processing rate
- Queue depth
- Database query performance
- Active tokens per user (avg, p95, p99)

Alerting Rules:

Critical:
- Delivery success rate < 90%
- Worker processing stopped
- Database connection pool exhausted

Warning:
- Delivery success rate < 95%
- Queue depth > 1000
- API response time > 500ms
- Retry queue growing

Logging:

Log with structured format (JSON):
{
  "timestamp": "2025-10-22T10:30:00Z",
  "level": "info",
  "service": "push-worker",
  "notification_id": "uuid",
  "user_id": "uuid",
  "tokens_count": 2,
  "success_count": 2,
  "failure_count": 0,
  "duration_ms": 150
}

5. Error Handling

Client-Side:

Retry strategy:
- Max 3 attempts
- Exponential backoff (1s, 2s, 4s)
- Store failed tokens locally
- Retry on next app startup
- Show user-friendly errors

Server-Side:

Graceful degradation:
- If FCM/APNs down: Queue for later
- If database slow: Circuit breaker pattern
- If rate limited: Backoff and retry
- Log all errors with context

6. Token Cleanup

-- Scheduled job (daily)
DELETE FROM device_tokens
WHERE is_active = false
  AND updated_at < NOW() - INTERVAL '90 days';

-- Mark stale tokens inactive (not used in 60 days)
UPDATE device_tokens
SET is_active = false
WHERE is_active = true
  AND last_used_at < NOW() - INTERVAL '60 days';

Advanced Features

1. Topic-Based Notifications

Use Case: Broadcast to user segments

Implementation:
- Client subscribes tokens to topics
- Server sends single message to topic
- Push service handles fanout

Example Topics:
- "all_users"
- "premium_users"
- "sports_updates"
- "region_us_west"

Benefit: Efficient broadcasting without looping through tokens

2. Scheduled Notifications

-- Add scheduled_for field
ALTER TABLE notifications 
ADD COLUMN scheduled_for TIMESTAMP WITH TIME ZONE;

-- Worker checks for due notifications
SELECT id FROM notifications
WHERE status = 'scheduled'
  AND scheduled_for <= NOW()
ORDER BY scheduled_for ASC
LIMIT 100;

3. User Preferences

CREATE TABLE user_notification_preferences (
    user_id UUID PRIMARY KEY REFERENCES users(id),
    enabled_types JSONB, -- ["message", "alert", "promotion"]
    quiet_hours_start TIME,
    quiet_hours_end TIME,
    timezone VARCHAR(50),
    frequency_limit INTEGER, -- max per day
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Check before sending
IF notification.type NOT IN user_preferences.enabled_types:
    skip

IF current_time BETWEEN quiet_hours:
    skip or schedule for later

4. Rich Notifications

Android:
- Custom layouts
- Action buttons
- Large images
- Progress bars

iOS:
- Media attachments
- Custom actions
- Rich content

Web:
- Icons and images
- Action buttons
- Require interaction

5. Analytics & A/B Testing

Track events:
- notification_sent
- notification_delivered
- notification_opened
- notification_action_taken

Use for:
- A/B test different titles/bodies
- Optimize send times
- Measure campaign effectiveness
- User segmentation

Common Pitfalls & Solutions

PitfallSolution
Not handling token expirationAlways listen for refresh events and update server
Blocking API response waiting for FCMReturn immediately, send push asynchronously
No retry mechanismImplement background worker with exponential backoff
Poor database performanceAdd proper indexes, partition large tables
Not invalidating bad tokensCheck error codes and mark inactive automatically
Missing multi-device supportAllow multiple tokens per user
No rate limitingImplement per-user and per-endpoint limits
Storing all history foreverArchive old data, set retention policies

Testing Strategy

Unit Tests

Backend:

  • Token registration logic
  • Notification creation
  • Status updates
  • Error handling
  • Token invalidation

Client:

  • Token sync logic
  • Message parsing
  • UI state management
  • Retry logic

Integration Tests

Full flow:
1. Client registers token → Server stores
2. Admin creates notification → DB record created
3. Worker picks up notification → Sends to FCM/APNs
4. Client receives push → Shows notification
5. User taps → Navigates to screen
6. User marks as read → Server updates

Load Tests

Scenarios:
- 1000 notifications/second
- 10,000 concurrent users
- Token refresh storm (all users at once)
- Database failover
- Push service throttling

Implementation Checklist

  • [ ] Set up FCM/APNs in push service provider
  • [ ] Create database tables with indexes
  • [ ] Implement token registration API
  • [ ] Build push delivery service
  • [ ] Add background retry worker
  • [ ] Create notifications CRUD API
  • [ ] Implement client-side token manager
  • [ ] Build notification history UI
  • [ ] Add authentication & authorization
  • [ ] Implement rate limiting
  • [ ] Set up monitoring and alerts
  • [ ] Add structured logging
  • [ ] Implement token cleanup job
  • [ ] Write unit tests
  • [ ] Write integration tests
  • [ ] Load test the system
  • [ ] Document API for team
  • [ ] Create runbook for operations

Conclusion

This architecture provides a solid foundation for push notifications that:

✅ Scales from day one - Handles growth without major refactoring
✅ Provides great UX - Users can view and manage notification history
✅ Ensures reliability - Automatic retries and error handling
✅ Maintains observability - Full tracking and monitoring
✅ Works anywhere - Language and framework agnostic

Next Phase Features

Once the core is solid, consider adding:

  • Topic subscriptions for efficient broadcasts
  • Scheduled notifications with timezone support
  • User preference management for notification control
  • Rich media notifications with images and actions
  • Analytics dashboard for engagement insights
  • A/B testing framework for optimization
  • Multi-language support with localization

Resources


Have questions or suggestions? Feel free to reach out!

Read more