📱 Push Sender¶
High-performance push notification delivery service for mobile devices
Overview¶
The Push Sender service is responsible for delivering push notifications to mobile devices across iOS, Android, and web platforms. It provides high-throughput, reliable delivery with real-time tracking and optimization capabilities.
🏗️ Architecture¶
Core Responsibilities¶
- Push Notification Delivery: Send notifications to iOS (APNs) and Android (FCM) devices
- Device Token Management: Validate, refresh, and manage device tokens
- Batch Processing: Efficient batch delivery for high-volume campaigns
- Delivery Tracking: Real-time delivery status and analytics
- Rate Limiting: Platform-specific rate limiting and throttling
- A/B Testing: Support for notification A/B testing and optimization
Technology Stack¶
- Node.js: Runtime environment
- Firebase Cloud Messaging (FCM): Android push notifications
- Apple Push Notification Service (APNs): iOS push notifications
- Web Push API: Browser-based push notifications
- Redis: Token caching and rate limiting
- MongoDB: Delivery tracking and analytics
🔧 Configuration¶
Server Details¶
- Server:
ovh-pushsender - Port: 3000 (HTTP), 3001 (HTTPS)
- Environment: Production, Staging, Development
Environment Variables¶
# Firebase Configuration
FIREBASE_PROJECT_ID=appgain-push
FIREBASE_PRIVATE_KEY=ask your direct manager for the access
FIREBASE_CLIENT_EMAIL=ask your direct manager for the access
# Apple Push Configuration
APNS_KEY_ID=ask your direct manager for the access
APNS_TEAM_ID=ask your direct manager for the access
APNS_PRIVATE_KEY=ask your direct manager for the access
APNS_BUNDLE_ID=com.appgain.app
# Redis Configuration
REDIS_HOST=ovh-redis
REDIS_PORT=6379
REDIS_PASSWORD=ask your direct manager for the access
# MongoDB Configuration
MONGODB_URI=mongodb://ovh-mongo-master:27017/push_sender
# Performance Configuration
BATCH_SIZE=1000
MAX_CONCURRENT_REQUESTS=100
RATE_LIMIT_PER_SECOND=1000
# Monitoring
SENTRY_DSN=ask your direct manager for the access
PROMETHEUS_ENABLED=true
LOG_LEVEL=info
📱 Platform Support¶
Android (FCM)¶
// FCM Configuration
const admin = require('firebase-admin');
const serviceAccount = {
projectId: process.env.FIREBASE_PROJECT_ID,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
clientEmail: process.env.FIREBASE_CLIENT_EMAIL
};
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const messaging = admin.messaging();
iOS (APNs)¶
// APNs Configuration
const apn = require('apn');
const apnProvider = new apn.Provider({
token: {
key: process.env.APNS_PRIVATE_KEY,
keyId: process.env.APNS_KEY_ID,
teamId: process.env.APNS_TEAM_ID
},
production: process.env.NODE_ENV === 'production'
});
Web Push¶
// Web Push Configuration
const webpush = require('web-push');
webpush.setVapidDetails(
'mailto:notifications@appgain.io',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
📊 API Endpoints¶
Notification Delivery¶
# Send Single Notification
POST /api/notifications/send
{
"deviceToken": "device_token",
"platform": "ios|android|web",
"title": "Notification Title",
"body": "Notification Body",
"data": {
"custom_key": "custom_value"
},
"options": {
"badge": 1,
"sound": "default",
"priority": "high"
}
}
# Send Batch Notifications
POST /api/notifications/batch
{
"notifications": [
{
"deviceToken": "token1",
"platform": "ios",
"title": "Title 1",
"body": "Body 1"
},
{
"deviceToken": "token2",
"platform": "android",
"title": "Title 2",
"body": "Body 2"
}
]
}
# Send to Topic
POST /api/notifications/topic
{
"topic": "news",
"title": "Breaking News",
"body": "Latest updates available",
"data": {
"news_id": "123"
}
}
Device Token Management¶
# Register Device Token
POST /api/tokens/register
{
"userId": "user_id",
"deviceToken": "device_token",
"platform": "ios|android|web",
"appVersion": "1.0.0",
"deviceInfo": {
"model": "iPhone 12",
"os": "iOS 15.0"
}
}
# Update Device Token
PUT /api/tokens/:tokenId
{
"userId": "user_id",
"platform": "ios",
"appVersion": "1.1.0"
}
# Delete Device Token
DELETE /api/tokens/:tokenId
# Get User Tokens
GET /api/tokens/user/:userId
Analytics & Reporting¶
# Get Delivery Statistics
GET /api/analytics/delivery?startDate=2024-01-01&endDate=2024-01-31
# Get Platform Statistics
GET /api/analytics/platforms?period=daily
# Get Error Analysis
GET /api/analytics/errors?platform=ios&limit=10
# Get Performance Metrics
GET /api/analytics/performance?metric=latency&period=hourly
🔄 Delivery Flow¶
Single Notification Flow¶
// Notification processing
async function sendNotification(notification) {
const { deviceToken, platform, title, body, data, options } = notification;
try {
// Validate device token
const isValid = await validateDeviceToken(deviceToken, platform);
if (!isValid) {
throw new Error('Invalid device token');
}
// Prepare notification payload
const payload = preparePayload(platform, title, body, data, options);
// Send notification
let result;
switch (platform) {
case 'ios':
result = await sendToAPNs(deviceToken, payload);
break;
case 'android':
result = await sendToFCM(deviceToken, payload);
break;
case 'web':
result = await sendToWebPush(deviceToken, payload);
break;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
// Track delivery
await trackDelivery(notification, result);
return result;
} catch (error) {
// Handle errors
await handleDeliveryError(notification, error);
throw error;
}
}
Batch Processing Flow¶
// Batch notification processing
async function sendBatchNotifications(notifications) {
const batches = groupByPlatform(notifications);
const results = [];
for (const [platform, batch] of Object.entries(batches)) {
const platformResults = await processBatch(platform, batch);
results.push(...platformResults);
}
return results;
}
async function processBatch(platform, notifications) {
const batchSize = getBatchSize(platform);
const batches = chunk(notifications, batchSize);
const results = [];
for (const batch of batches) {
const batchResult = await sendBatchToPlatform(platform, batch);
results.push(...batchResult);
// Rate limiting
await delay(getRateLimitDelay(platform));
}
return results;
}
🗄️ Database Schema¶
Device Tokens Collection¶
{
"_id": "ObjectId",
"token": "device_token_string",
"userId": "user_id",
"platform": "ios|android|web",
"appVersion": "1.0.0",
"deviceInfo": {
"model": "iPhone 12",
"os": "iOS 15.0",
"manufacturer": "Apple"
},
"isActive": true,
"lastUsed": "2024-01-01T00:00:00Z",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
Delivery Logs Collection¶
{
"_id": "ObjectId",
"notificationId": "notification_id",
"deviceToken": "device_token",
"platform": "ios|android|web",
"status": "sent|delivered|failed|invalid_token",
"errorCode": "error_code_if_failed",
"errorMessage": "error_message_if_failed",
"deliveryTime": "2024-01-01T00:00:00Z",
"responseTime": 150, // milliseconds
"retryCount": 0,
"createdAt": "2024-01-01T00:00:00Z"
}
Analytics Collection¶
{
"_id": "ObjectId",
"date": "2024-01-01",
"platform": "ios|android|web",
"metrics": {
"totalSent": 1000,
"totalDelivered": 950,
"totalFailed": 50,
"averageResponseTime": 120,
"successRate": 0.95
},
"hourlyBreakdown": [
{
"hour": 0,
"sent": 100,
"delivered": 95,
"failed": 5
}
],
"createdAt": "2024-01-01T00:00:00Z"
}
📈 Performance & Optimization¶
Rate Limiting¶
// Platform-specific rate limits
const RATE_LIMITS = {
ios: {
requestsPerSecond: 1000,
batchSize: 1000,
retryDelay: 1000
},
android: {
requestsPerSecond: 1000,
batchSize: 500,
retryDelay: 500
},
web: {
requestsPerSecond: 100,
batchSize: 100,
retryDelay: 2000
}
};
// Rate limiting implementation
const rateLimiter = new Map();
function checkRateLimit(platform) {
const now = Date.now();
const limit = RATE_LIMITS[platform];
if (!rateLimiter.has(platform)) {
rateLimiter.set(platform, []);
}
const requests = rateLimiter.get(platform);
const validRequests = requests.filter(time => now - time < 1000);
if (validRequests.length >= limit.requestsPerSecond) {
return false;
}
validRequests.push(now);
rateLimiter.set(platform, validRequests);
return true;
}
Token Validation & Refresh¶
// Token validation
async function validateDeviceToken(token, platform) {
const cacheKey = `token:${token}`;
// Check cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Validate with platform
let isValid = false;
try {
switch (platform) {
case 'ios':
isValid = await validateAPNsToken(token);
break;
case 'android':
isValid = await validateFCMToken(token);
break;
case 'web':
isValid = await validateWebPushToken(token);
break;
}
} catch (error) {
console.error('Token validation error:', error);
isValid = false;
}
// Cache result
await redis.setex(cacheKey, 3600, JSON.stringify(isValid));
return isValid;
}
// Token refresh
async function refreshDeviceToken(oldToken, newToken, platform) {
// Update database
await DeviceToken.findOneAndUpdate(
{ token: oldToken },
{
token: newToken,
updatedAt: new Date()
}
);
// Invalidate cache
await redis.del(`token:${oldToken}`);
await redis.del(`token:${newToken}`);
// Update analytics
await updateTokenRefreshStats(platform);
}
Batch Optimization¶
// Batch size optimization
function getOptimalBatchSize(platform, notificationCount) {
const baseSize = RATE_LIMITS[platform].batchSize;
const maxSize = Math.min(baseSize, notificationCount);
// Adjust based on current load
const currentLoad = getCurrentLoad(platform);
if (currentLoad > 0.8) {
return Math.floor(maxSize * 0.5);
}
return maxSize;
}
// Parallel processing
async function sendBatchParallel(notifications, platform) {
const batchSize = getOptimalBatchSize(platform, notifications.length);
const batches = chunk(notifications, batchSize);
const promises = batches.map(batch =>
sendBatchToPlatform(platform, batch)
);
const results = await Promise.allSettled(promises);
return results.flatMap(result =>
result.status === 'fulfilled' ? result.value : []
);
}
🔍 Monitoring & Analytics¶
Real-time Metrics¶
// Prometheus metrics
const prometheus = require('prom-client');
const notificationCounter = new prometheus.Counter({
name: 'push_notifications_total',
help: 'Total number of push notifications sent',
labelNames: ['platform', 'status']
});
const deliveryLatency = new prometheus.Histogram({
name: 'push_delivery_latency_seconds',
help: 'Push notification delivery latency',
labelNames: ['platform'],
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
// Record metrics
async function sendNotification(notification) {
const startTime = Date.now();
try {
const result = await sendToPlatform(notification);
// Record success
notificationCounter.labels(notification.platform, 'success').inc();
deliveryLatency.labels(notification.platform)
.observe((Date.now() - startTime) / 1000);
return result;
} catch (error) {
// Record failure
notificationCounter.labels(notification.platform, 'failed').inc();
throw error;
}
}
Health Checks¶
# Service health check
GET /health
# Response format
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0.0",
"services": {
"fcm": "connected",
"apns": "connected",
"redis": "connected",
"mongodb": "connected"
},
"metrics": {
"totalNotifications": 10000,
"successRate": 0.95,
"averageLatency": 120
}
}
Error Tracking¶
// Error categorization
const ERROR_CATEGORIES = {
INVALID_TOKEN: 'invalid_token',
RATE_LIMIT: 'rate_limit',
NETWORK_ERROR: 'network_error',
PLATFORM_ERROR: 'platform_error',
TIMEOUT: 'timeout'
};
// Error handling
async function handleDeliveryError(notification, error) {
const errorCategory = categorizeError(error);
// Log error
console.error('Push delivery error:', {
notificationId: notification.id,
deviceToken: notification.deviceToken,
platform: notification.platform,
error: error.message,
category: errorCategory
});
// Update analytics
await updateErrorStats(notification.platform, errorCategory);
// Handle specific errors
if (errorCategory === ERROR_CATEGORIES.INVALID_TOKEN) {
await handleInvalidToken(notification.deviceToken, notification.platform);
}
// Send to monitoring
if (process.env.SENTRY_DSN) {
Sentry.captureException(error, {
tags: {
platform: notification.platform,
errorCategory
},
extra: {
notification
}
});
}
}
🚀 Deployment¶
Docker Configuration¶
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Docker Compose¶
# docker-compose.yml
version: '3.8'
services:
push-sender:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- REDIS_HOST=redis
- MONGODB_URI=mongodb://mongo:27017/push_sender
depends_on:
- redis
- mongo
networks:
- appgain-net
deploy:
replicas: 3
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- appgain-net
mongo:
image: mongo:6.0
volumes:
- mongo-data:/data/db
networks:
- appgain-net
volumes:
redis-data:
mongo-data:
networks:
appgain-net:
driver: bridge
Kubernetes Deployment¶
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: push-sender
spec:
replicas: 3
selector:
matchLabels:
app: push-sender
template:
metadata:
labels:
app: push-sender
spec:
containers:
- name: push-sender
image: appgain/push-sender:latest
env:
- name: NODE_ENV
value: "production"
- name: REDIS_HOST
value: "redis-cluster"
- name: MONGODB_URI
value: "mongodb://mongo-cluster:27017/push_sender"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
🔧 Development¶
Local Development Setup¶
# Install dependencies
npm install
# Environment setup
cp .env.example .env
# Edit .env with local configuration
# Start development server
npm run dev
# Run tests
npm test
# Run linting
npm run lint
Testing Push Notifications¶
// Test notification sending
const testNotification = {
deviceToken: 'test_device_token',
platform: 'ios',
title: 'Test Notification',
body: 'This is a test notification',
data: {
test_key: 'test_value'
}
};
// Send test notification
const result = await sendNotification(testNotification);
console.log('Test result:', result);
// Test batch sending
const testBatch = Array(10).fill().map((_, i) => ({
deviceToken: `test_token_${i}`,
platform: 'android',
title: `Test ${i}`,
body: `Test notification ${i}`
}));
const batchResult = await sendBatchNotifications(testBatch);
console.log('Batch result:', batchResult);
🔒 Security¶
Token Security¶
// Token encryption
const crypto = require('crypto');
function encryptToken(token) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(algorithm, key);
let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decryptToken(encryptedToken) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'salt', 32);
const [ivHex, encrypted] = encryptedToken.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipher(algorithm, key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Input Validation¶
// Notification validation
const Joi = require('joi');
const notificationSchema = Joi.object({
deviceToken: Joi.string().required(),
platform: Joi.string().valid('ios', 'android', 'web').required(),
title: Joi.string().max(100).required(),
body: Joi.string().max(2000).required(),
data: Joi.object().optional(),
options: Joi.object({
badge: Joi.number().integer().min(0),
sound: Joi.string(),
priority: Joi.string().valid('high', 'normal', 'low')
}).optional()
});
function validateNotification(notification) {
const { error, value } = notificationSchema.validate(notification);
if (error) {
throw new Error(`Invalid notification: ${error.message}`);
}
return value;
}
📞 Support & Resources¶
Documentation¶
Monitoring Tools¶
- Sentry: Error tracking and performance monitoring
- Prometheus: Metrics collection and alerting
- Grafana: Dashboard visualization
- Custom Analytics: Delivery tracking and reporting
Development Resources¶
- GitHub Repository: Source code and issues
- Postman Collection: API testing
- Swagger Documentation: Interactive API docs
- Development Environment: Docker setup
Last updated: January 2024
Ask Chehab GPT