1. Overview

The Nimbly platform implements a comprehensive notification system supporting three primary channels:

  1. WhatsApp Business API - Via AiSensy service provider
  2. Email - Via SendGrid
  3. Push Notifications - Via Firebase Cloud Messaging (FCM)

The system is designed with a microservices architecture, utilizing message queues and cloud functions for scalable, asynchronous notification delivery.

Related Systems:


2. Architecture Overview

graph TB
    subgraph "Client Applications"
        WEB[Web App]
        MOBILE[Mobile App]
    end
    
    subgraph "API Gateway"
        GATEWAY[API Gateway<br/>Cloudflare Workers]
    end
    
    subgraph "Core Services"
        AUTH[api-auth]
        ISSUES[api-issues]
        REPORTS[api-reports]
        SCHEDULES[api-schedules]
        LMS[api-lms]
        SANDBOX[api-sandbox]
    end
    
    subgraph "Notification Services"
        WHATSAPP[api-whatsapp<br/>AiSensy Integration]
        FUNCTIONS[audit-functions<br/>Cloud Functions]
        PUBSUB[PubSub Service]
    end
    
    subgraph "External Services"
        SENDGRID[SendGrid API]
        AISENSY[AiSensy WhatsApp API]
        FCM[Firebase Cloud Messaging]
    end
    
    subgraph "Data Storage"
        FIREBASE[Firebase Realtime DB<br/>Push Tokens]
        MONGODB[MongoDB<br/>Notification Events]
    end
    
    WEB --> GATEWAY
    MOBILE --> GATEWAY
    GATEWAY --> AUTH
    GATEWAY --> ISSUES
    GATEWAY --> REPORTS
    GATEWAY --> SCHEDULES
    
    ISSUES --> WHATSAPP
    REPORTS --> WHATSAPP
    SCHEDULES --> WHATSAPP
    
    ISSUES --> PUBSUB
    REPORTS --> PUBSUB
    AUTH --> SENDGRID
    LMS --> SENDGRID
    SANDBOX --> SENDGRID
    
    WHATSAPP --> AISENSY
    FUNCTIONS --> SENDGRID
    FUNCTIONS --> FCM
    PUBSUB --> FUNCTIONS
    
    MOBILE --> FIREBASE
    FUNCTIONS --> MONGODB

3. Notification Types

3.1. Report Notifications

  • Report generated
  • Report due
  • Report overdue
  • Daily recap
  • Weekly recap
  • Monthly recap

3.2. Issue Notifications

  • Issue created (audit/ad-hoc)
  • Status changes
  • Priority/severity changes
  • Member additions/removals
  • Comments added
  • Due date changes
  • Issue escalations
  • Department changes

3.3. Authentication Notifications

  • Two-factor authentication codes
  • User enrollment invitations
  • Enrollment approvals/rejections

3.4. Learning Management Notifications

  • Course enrollments
  • Syllabus enrollments

3.5. Sandbox/Trial Notifications

  • Trial account setup completion
  • Trial expiration warnings
  • Trial ended notifications

4. Libraries and Dependencies

4.1. Core Libraries

LibraryVersionPurposeUsed In
@sendgrid/mailLatestEmail deliveryAll services with email
firebase-adminLatestPush notifications & data storageaudit-functions, entity-node
axiosLatestHTTP requestsapi-whatsapp, api-issues
express4.xWeb frameworkAll API services
@google-cloud/pubsubLatestMessage queuenimbly-pubsub
jsonwebtokenLatestJWT authenticationAll services
mongooseLatestMongoDB ODMentity-node

4.2. Internal Libraries

LibraryPurpose
@nimbly-technologies/nimbly-commonShared types and utilities
@nimbly-technologies/entity-nodeDatabase repositories
@nimbly-technologies/nimbly-backend-utilsBackend utilities and middleware
@nimbly-technologies/nimbly-accessAccess control

4.3. Mobile Libraries

LibraryPurpose
@react-native-firebase/messagingFCM integration for React Native
expo-[notifications](NotificationOverview.md)Push notification handling
@react-native-async-storage/async-storageToken storage

5. API Endpoints

5.1. WhatsApp API Endpoints

MethodEndpointDescriptionAuthentication
POST/[report](../../Reports/ReportsOverview.md)-notificationSend WhatsApp notification for reportsJWT Required
POST/issue-notificationSend WhatsApp notification for issuesJWT Required

5.2. Internal Notification Triggers

ServiceTriggerChannelDescription
api-issuesIssue eventsEmail, WhatsApp, PushTriggered on issue lifecycle events
api-authUser enrollmentEmailUser invitation and approval
api-lmsCourse enrollmentEmailCourse/syllabus enrollment notifications
api-sandboxTrial eventsEmailTrial account lifecycle notifications
audit-functionsScheduled jobsEmail, PushWeekly stats, report generation

5.3. Mobile App Endpoints

MethodEndpointDescription
PUT/v1.0/users/pushTokenUpdate user’s FCM token

6. WhatsApp Notifications

6.1. Architecture

sequenceDiagram
    participant Service as Core Service<br/>(Issues/Reports)
    participant WhatsApp as WhatsApp API<br/>Service
    participant User as User Repository
    participant Unsubscribe as Unsubscribe<br/>Settings
    participant AiSensy as AiSensy API
    
    Service->>WhatsApp: POST /report-notification
    WhatsApp->>User: Get user details & WhatsApp number
    WhatsApp->>Unsubscribe: Check unsubscribe settings
    
    alt User unsubscribed
        WhatsApp-->>Service: Return error (unsubscribed)
    else User subscribed
        WhatsApp->>WhatsApp: Determine campaign type<br/>(basic/advanced)
        WhatsApp->>WhatsApp: Build template parameters
        WhatsApp->>AiSensy: POST /api/v2
        AiSensy-->>WhatsApp: Response
        WhatsApp-->>Service: Success response
    end

6.2. WhatsApp Campaign Types

Report Campaigns

// Basic Templates (text only with link)
REPORT_GENERATED_BASIC
REPORT_OVERDUE_BASIC
REPORT_DUE_BASIC
REPORT_RECAP_DAILY_BASIC
REPORT_RECAP_WEEKLY_BASIC
REPORT_RECAP_MONTHLY_BASIC
 
// Advanced Templates (with file attachments)
REPORT_GENERATED_ADV
REPORT_OVERDUE_ADV
REPORT_RECAP_DAILY_ADV
REPORT_RECAP_WEEKLY_ADV
REPORT_RECAP_MONTHLY_ADV

Issue Campaigns

// All issue templates are currently basic (text only)
ISSUE_AUDIT_BASIC
ISSUE_ADHOC_BASIC
ISSUE_CHANGE_STATUS_BASIC
ISSUE_CHANGE_PRIORITY_BASIC
ISSUE_CHANGE_FLAG_BASIC
ISSUE_CHANGE_DEPARTMENT_BASIC
ISSUE_CHANGE_USER_BASIC
ISSUE_CHANGE_DUEDATE_BASIC
ISSUE_ADD_MEMBER_BASIC
ISSUE_REMOVE_MEMBER_BASIC
ISSUE_NEW_COMMENT_BASIC
ISSUE_NEAR_DUEDATE_BASIC
ISSUE_OVERDUE_BASIC
ISSUE_ESCALATED_BASIC
ISSUE_RECAP_DAILY_BASIC/ADV
ISSUE_RECAP_WEEKLY_BASIC/ADV
ISSUE_RECAP_MONTHLY_BASIC/ADV

6.3. WhatsApp Implementation Details

// WhatsApp notification payload structure
interface WhatsAppNotificationPayload {
    apiKey: string;
    campaignName: string;
    destination: string;          // User's WhatsApp number
    userName: string;
    source: string;              // Organization ID
    templateParams: string[];    // Template variables
    media?: {                    // For advanced notifications
        url: string;
        filename: string;
    };
}
 
// Example implementation
async sendReportNotification(context: Context<UserAuth>, data: any) {
    // 1. Validate notification type
    if (!reportNotificationType.includes(data.notificationType)) {
        return { error: 'invalid', message: 'wrong notification type' };
    }
    
    // 2. Get user details
    const user = await this.userRepo.findOneByID(data.userID);
    const destination = user.whatsappNumber;
    
    // 3. Check unsubscribe settings
    const unsubscribed = await this.unSubscribeNotificationsSettingsRepo
        .getByOrganizationIDAndPreferences(user.organizationID, undefined, true);
    
    // 4. Determine campaign type (basic/advanced)
    const campaignKey = user.whatsappAdvanceNotification 
        ? `${notificationType}-adv` 
        : `${notificationType}-basic`;
    
    // 5. Build and send request to AiSensy
    const response = await this.api.post('/api/v2', requestBody);
}

7. Email Notifications

7.1. Architecture

sequenceDiagram
    participant Service as Core Service
    participant SendGrid as SendGrid Config
    participant Template as Email Template
    participant Queue as PubSub Queue
    participant CloudFn as Cloud Function
    participant API as SendGrid API
    
    Service->>SendGrid: Initialize with API key
    Service->>Template: Generate HTML template
    Service->>Queue: Publish to emailNotification:sendBulkEmail
    Queue->>CloudFn: Trigger function
    CloudFn->>API: Send email via SendGrid
    API-->>CloudFn: Response

7.2. Email Configuration

// SendGrid initialization
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_KEY);
 
// Standard email structure
const mail: MailDataRequired = {
    to: recipient.email,
    from: 'Nimbly <noreply@hellonimbly.com>',
    subject: 'Subject Line',
    text: 'For the best experience, please view this email in HTML',
    html: emailTemplate,
};
 
// Send email
await sgMail.send(mail);

7.3. Email Templates

Issue Email Templates

  • issueCreationEmailTemplate.ts - New issue creation
  • adhocIssueCreationEmailTemplate.ts - Ad-hoc issue creation
  • issueFlagChangeEmailTemplate.ts - Flag/status changes
  • issueMemberEmailTemplate.ts - Member additions/removals
  • issueEscalationEmailTemplate.ts - Issue escalations
  • bulkStatusUpdateEmailTemplate.ts - Bulk status updates
  • issueApprovalEmailTemplate.ts - Approval requests
  • requestApprovedEmailTemplate.ts - Approval granted
  • requestRejectedEmailTemplate.ts - Approval rejected

Authentication Email Templates

  • twoFactorAuth.ts - 2FA codes
  • newEnrollment.ts - New user invitations
  • approvedEnrollment.ts - Enrollment approved
  • rejectedEnrollment.ts - Enrollment rejected

Learning Management Templates

  • courseEnrollmentEmailTemplate.ts - Course/syllabus enrollments

Sandbox/Trial Templates

  • Trial account setup completed
  • Trial expiration warnings (7 days, 3 days, 1 day)
  • Trial ended notifications

7.4. Email Notification Flow

// Example: Issue comment notification flow
async addIssueMessage(ctx: Context<UserAuth>, data: CreateIssueMessageRequest) {
    // 1. Get notification settings
    const notificationSettings = await this.notificationSettingsRepository
        .getByOrganizationID(organizationID);
    
    // 2. Check if notifications are enabled
    if (!notificationSettings?.disabled) {
        const config = notificationSettings.configs[NotificationTrigger.ISSUE_ADD_COMMENT];
        
        // 3. Get recipients based on configuration
        if (config?.channels.email) {
            const recipients = await this.getRecipientsDetails(ctx, issue, config.recipients);
            
            // 4. Generate emails for each recipient
            const emails = recipients.map(member => 
                generateEmail('addNewComment', issue, site, member, [inviter.displayName])
            );
            
            // 5. Send via PubSub queue
            await this.pubsubNotificationRepository.publish('emailNotification:sendBulkEmail', {
                payload: { content: emails, organizationID }
            });
        }
    }
}

8. Push Notifications

8.1. Architecture

graph TB
    subgraph "Mobile App"
        APP[React Native App]
        EXPO[Expo Notifications]
        FIREBASE_MSG[Firebase Messaging]
    end
    
    subgraph "Backend Services"
        API[API Services]
        PUBSUB[PubSub Queue]
        CLOUD_FN[Cloud Functions]
    end
    
    subgraph "Firebase Services"
        FCM[Firebase Cloud Messaging]
        RTDB[Realtime Database<br/>Token Storage]
    end
    
    subgraph "Device Platforms"
        IOS[iOS - APNS]
        ANDROID[Android - FCM]
    end
    
    APP --> EXPO
    EXPO --> FIREBASE_MSG
    FIREBASE_MSG --> RTDB
    
    API --> PUBSUB
    PUBSUB --> CLOUD_FN
    CLOUD_FN --> FCM
    
    FCM --> IOS
    FCM --> ANDROID
    
    RTDB -.-> CLOUD_FN

8.2. Push Token Management

// Token storage structure in Firebase Realtime Database
/userPushToken/{userID}: string
 
// Token registration (Mobile App)
async registerForPushNotifications() {
    // 1. Request permissions
    const { status } = await Notifications.requestPermissionsAsync();
    
    // 2. Get Expo push token
    const token = await Notifications.getExpoPushTokenAsync();
    
    // 3. Update token on server
    await api.put('/v1.0/users/pushToken', { 
        pushToken: token.data 
    });
}
 
// Token refresh handler
messaging().onTokenRefresh(async (token) => {
    await updateUserPushToken(token);
});

8.3. Push Notification Implementation

// Firebase notification sending
async sendNotificationToUsers(data: SendNotificationRequest) {
    const { uids, title, body, notificationData } = data;
    
    // 1. Get user tokens
    const userTokens = await Promise.all(
        uids.map(uid => this.userPushTokenRepository.findByUserID(uid))
    );
    
    // 2. Build notification message
    const message: admin.messaging.Message = {
        notification: { title, body },
        apns: {
            payload: {
                aps: {
                    badge: 0,
                    sound: 'default',
                }
            }
        },
        android: {
            notification: {
                sound: 'default',
            }
        },
        token: userToken,
        data: notificationData
    };
    
    // 3. Send via FCM
    await this.fbMessaging.send(message);
    
    // 4. Handle invalid tokens
    if (error.code === 'messaging/invalid-registration-token') {
        await this.userPushTokenRepository.removeByUserID(targetUserID);
    }
}

8.4. Background Notification Handling

// Mobile app background handler
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
    // Handle notification when app is in background
    if (remoteMessage.data?.action === 'FORCE_LOGOUT') {
        await handleForceLogout();
    }
    
    // Show local notification
    await Notifications.scheduleNotificationAsync({
        content: {
            title: remoteMessage.notification.title,
            body: remoteMessage.notification.body,
            data: remoteMessage.data,
        },
        trigger: null,
    });
});

9. Notification Flows

9.1. Issue Creation Flow

sequenceDiagram
    participant User as User
    participant API as Issues API
    participant Settings as Notification Settings
    participant Recipients as Recipient Service
    participant Email as Email Queue
    participant WhatsApp as WhatsApp API
    participant Push as Push Queue
    
    User->>API: Create Issue
    API->>Settings: Get notification config
    
    alt Notifications enabled
        API->>Recipients: Get recipients based on config
        
        par Email Channel
            API->>Email: Publish bulk email job
        and WhatsApp Channel
            API->>WhatsApp: Send WhatsApp notifications
        and Push Channel
            API->>Push: Publish push notification job
        end
    end
    
    API-->>User: Issue created

9.2. Report Generation Flow

sequenceDiagram
    participant Scheduler as Scheduler
    participant Functions as Cloud Functions
    participant Report as Report Generator
    participant Storage as File Storage
    participant Notif as Notification Service
    participant WhatsApp as WhatsApp API
    participant Email as Email Service
    
    Scheduler->>Functions: Trigger report generation
    Functions->>Report: Generate report
    Report->>Storage: Save report file
    Report->>Functions: Return file URL
    
    Functions->>Notif: Determine recipients
    
    par WhatsApp Notification
        Functions->>WhatsApp: Send with file URL/attachment
    and Email Notification
        Functions->>Email: Send with file attachment
    end

9.3. Notification Recipient Resolution

graph TD
    START[Notification Trigger] --> CONFIG[Get Notification Config]
    CONFIG --> CHECK{Config Exists?}
    
    CHECK -->|No| END[No Notification]
    CHECK -->|Yes| RECIPIENTS[Determine Recipients]
    
    RECIPIENTS --> SITE[Site Members]
    RECIPIENTS --> DEPT[Department Members]
    RECIPIENTS --> OWNER[Issue Owner]
    RECIPIENTS --> SPECIFIC[Specific Users]
    
    SITE --> VISIBILITY[Check Data Visibility]
    DEPT --> VISIBILITY
    OWNER --> VISIBILITY
    SPECIFIC --> VISIBILITY
    
    VISIBILITY --> FILTER[Filter by Permissions]
    FILTER --> CHANNELS[Check Enabled Channels]
    
    CHANNELS --> EMAIL{Email?}
    CHANNELS --> WHATSAPP{WhatsApp?}
    CHANNELS --> PUSH{Push?}
    
    EMAIL -->|Yes| SEND_EMAIL[Send Email]
    WHATSAPP -->|Yes| SEND_WA[Send WhatsApp]
    PUSH -->|Yes| SEND_PUSH[Send Push]

10. Implementation Details

10.1. Notification Settings Structure

interface NotificationSettings {
    organizationID: string;
    disabled: boolean;
    configs: {
        [key in NotificationTrigger]: {
            channels: {
                email: boolean;
                push: boolean;
                whatsapp: boolean;
            };
            recipients: NotificationRecipient[];
        };
    };
}
 
enum NotificationTrigger {
    ISSUE_CREATED = 'ISSUE_CREATED',
    ISSUE_STATUS_CHANGED = 'ISSUE_STATUS_CHANGED',
    ISSUE_PRIORITY_CHANGED = 'ISSUE_PRIORITY_CHANGED',
    ISSUE_ADD_MEMBER = 'ISSUE_ADD_MEMBER',
    ISSUE_REMOVE_MEMBER = 'ISSUE_REMOVE_MEMBER',
    ISSUE_ADD_COMMENT = 'ISSUE_ADD_COMMENT',
    ISSUE_DUE_DATE_CHANGED = 'ISSUE_DUE_DATE_CHANGED',
    ISSUE_ESCALATED = 'ISSUE_ESCALATED',
    // ... more triggers
}
 
enum NotificationRecipient {
    SITE_MEMBERS = 'SITE_MEMBERS',
    DEPARTMENT_MEMBERS = 'DEPARTMENT_MEMBERS',
    ISSUE_OWNER = 'ISSUE_OWNER',
    ISSUE_MEMBERS = 'ISSUE_MEMBERS',
    SPECIFIC_USERS = 'SPECIFIC_USERS',
}

10.2. Unsubscribe Management

interface UnsubscribeNotificationSettings {
    userID: string;
    organizationID: string;
    unsubscribeEmail: boolean;
    unsubscribeWhatsapp: boolean;
    unsubscribePush: boolean;
}
 
// Check before sending
const unsubscribed = await unSubscribeRepo
    .getByOrganizationIDAndPreferences(organizationID, undefined, true);
 
if (unsubscribed.includes(userID)) {
    return { error: 'unsubscribed' };
}

10.3. Data Visibility Rules

// Determine data visibility for notifications
function getDataVisibility(user: User, site: Site): DataVisibility {
    if (user.sites.includes(site.siteID)) {
        return DataVisibility.FULL;
    }
    if (user.departments.some(dept => site.departments.includes(dept))) {
        return DataVisibility.LIMITED;
    }
    return DataVisibility.NONE;
}
 
// Filter notification content based on visibility
function filterNotificationContent(content: any, visibility: DataVisibility) {
    switch (visibility) {
        case DataVisibility.FULL:
            return content;
        case DataVisibility.LIMITED:
            return { ...content, sensitiveData: null };
        case DataVisibility.NONE:
            return null;
    }
}

10.4. Queue-Based Processing

// PubSub topic structure
const NOTIFICATION_TOPICS = {
    EMAIL: 'emailNotification:sendBulkEmail',
    PUSH: 'issue:sendNotification',
    REPORT: 'report:sendNotification',
};
 
// Publishing to queue
await pubsubRepository.publish(NOTIFICATION_TOPICS.EMAIL, {
    payload: {
        content: emails,
        organizationID,
        metadata: {
            trigger: 'ISSUE_CREATED',
            timestamp: new Date().toISOString(),
        }
    }
});
 
// Queue handler in Cloud Functions
exports.handleEmailNotification = functions.pubsub
    .topic('emailNotification-sendBulkEmail')
    .onPublish(async (message) => {
        const { content, organizationID } = message.json.payload;
        
        // Batch process emails
        const batches = arrayToBatches(content, 100);
        for (const batch of batches) {
            await sgMail.send(batch);
        }
    });

10.5. Rate Limiting and Throttling

// WhatsApp API rate limiting
const WHATSAPP_RATE_LIMITS = {
    MESSAGES_PER_SECOND: 10,
    MESSAGES_PER_MINUTE: 600,
};
 
// Implement rate limiting
class RateLimiter {
    private queue: Array<() => Promise<any>> = [];
    private processing = false;
    
    async add(fn: () => Promise<any>) {
        this.queue.push(fn);
        if (!this.processing) {
            this.process();
        }
    }
    
    private async process() {
        this.processing = true;
        while (this.queue.length > 0) {
            const batch = this.queue.splice(0, WHATSAPP_RATE_LIMITS.MESSAGES_PER_SECOND);
            await Promise.all(batch.map(fn => fn()));
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
        this.processing = false;
    }
}

11. Security and Authentication

11.1. JWT Authentication

// Middleware for API authentication
const authMiddleware = new AuthMiddleware<UserAuth>(process.env.JWT_SECRET!, jwt);
 
// Apply to routes
router.post('/report-notification',
    authMiddleware.expressHandler(),
    expressMiddlewareHandler(controller.sendReportNotification)
);

11.2. API Key Management

// Environment variables
SENDGRID_KEY=SG.xxxxxxxxxxxx          // SendGrid API key
AISENSY_KEY=xxxxxxxxxxxxxxxxx         // AiSensy WhatsApp API key
JWT_SECRET=xxxxxxxxxxxxxxxxxx         // JWT signing secret
WHATSAPP_URL=https://internal-api     // Internal WhatsApp service URL

11.3. Data Encryption

  • User push tokens stored encrypted in Firebase
  • WhatsApp numbers stored with user consent
  • Notification payloads encrypted in transit (HTTPS)
  • Sensitive data filtered based on user permissions

12. Error Handling

12.1. WhatsApp Error Handling

try {
    const response = await this.api.post('/api/v2', requestBody);
    if (response.status === 200) {
        log.info(`Successfully sent to AiSensy for userID:${userID}`);
        return { data: { message: 'Success', response: response.data } };
    }
} catch (error) {
    log.error(`[AiSensy][WhatsappAPI] Error: ${error}`);
    
    // Categorize errors
    if (error.response?.status === 400) {
        return { error: 'invalid_request', message: 'Invalid WhatsApp template' };
    }
    if (error.response?.status === 401) {
        return { error: 'unauthorized', message: 'Invalid API key' };
    }
    if (error.response?.status === 429) {
        return { error: 'rate_limit', message: 'Rate limit exceeded' };
    }
    
    throw error;
}

12.2. Email Error Handling

// SendGrid error handling
try {
    await sgMail.send(mail);
} catch (error) {
    if (error.response) {
        const { statusCode, body } = error.response;
        
        if (statusCode === 401) {
            log.error('SendGrid authentication failed');
        } else if (statusCode === 413) {
            log.error('Email payload too large');
        } else if (statusCode === 429) {
            log.error('SendGrid rate limit exceeded');
        }
    }
    
    // Retry logic for transient errors
    if (isTransientError(error)) {
        await retryWithBackoff(() => sgMail.send(mail));
    }
}

12.3. Push Notification Error Handling

// FCM token validation
if (error.code === 'messaging/invalid-registration-token' ||
    error.code === 'messaging/registration-token-not-registered') {
    // Remove invalid token
    await this.userPushTokenRepository.removeByUserID(targetUserID);
    log.info(`Removed invalid token for user ${targetUserID}`);
}
 
// Retry logic for FCM
const MAX_RETRIES = 3;
let retries = 0;
 
while (retries < MAX_RETRIES) {
    try {
        await this.fbMessaging.send(message);
        break;
    } catch (error) {
        if (error.code === 'messaging/server-unavailable') {
            retries++;
            await new Promise(resolve => setTimeout(resolve, Math.pow(2, retries) * 1000));
        } else {
            throw error;
        }
    }
}

13. Troubleshooting Guide

13.1. Common Issues

1. WhatsApp Messages Not Delivered

  • Check user has valid WhatsApp number
  • Verify user hasn’t unsubscribed
  • Confirm AiSensy API key is valid
  • Check template is approved
  • Verify rate limits not exceeded

2. Push Notifications Not Received

  • Confirm user has valid push token
  • Check app permissions
  • Verify FCM configuration
  • Test with FCM console
  • Check background restrictions

3. Email Delivery Issues

  • Verify SendGrid API key
  • Check spam folder
  • Confirm email address validity
  • Review SendGrid logs
  • Check rate limits

13.2. Debug Logging

// Enable debug mode
if (process.env.DEBUG_NOTIFICATIONS) {
    log.debug('[Notification Debug]', {
        request: requestBody,
        response: response.data,
        headers: response.headers,
    });
}

14. Conclusion

The Nimbly notification system provides a robust, scalable solution for multi-channel notifications. By leveraging microservices architecture, message queues, and external service providers, the system ensures reliable delivery while maintaining flexibility. The implementation follows best practices for security, performance, and user experience, making it suitable for enterprise-scale deployments.