Skip to main content
Important: This system uses ONLY Firebase Cloud Messaging (FCM) and Apple Push Notification Service (APNs). Expo push service has been completely removed for full control and production reliability.

Overview

The Notifications API provides a comprehensive push notification system built specifically for production environments. It supports direct FCM and APNs integration, scheduled notifications, bulk messaging, and advanced debugging tools. Location: convex/shared/notifications.ts & convex/shared/notificationActions.ts

Key Features

FCM/APNs Direct

Direct integration with Firebase and Apple push services - no third-party dependencies

Scheduled Notifications

Send notifications at specific future times with automatic scheduling

Bulk Messaging

Efficiently send notifications to multiple users with batching and rate limiting

Advanced Debugging

Comprehensive debugging tools for troubleshooting delivery issues

Record Native Device Token ⭐

PRIMARY FUNCTION - Records a native device token (FCM/APNs) for direct push notifications.
deviceToken
string
required
Native FCM token (Android) or APNs token (iOS)
platform
'ios' | 'android'
required
Device platform
appVersion
string
App version for tracking
deviceInfo
object
Device information object with deviceId, deviceName, and osVersion
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

// Get NATIVE token (not Expo token)
const nativeDeviceToken = await Notifications.getDevicePushTokenAsync();

// Register with backend
await convex.mutation(api.shared.notifications.recordNativeDeviceToken, {
  deviceToken: nativeDeviceToken.data, // This is FCM/APNs token
  platform: Platform.OS === 'ios' ? 'ios' : 'android',
  appVersion: "1.0.0",
  deviceInfo: {
    deviceId: "device-123",
    deviceName: "Samsung Galaxy S21",
    osVersion: "Android 12"
  }
});
null

Send Notification to User ⭐

PRIMARY FUNCTION - Sends notifications to a user across all their devices via FCM/APNs.
userId
string
required
User ID to send notification to
title
string
required
Notification title
body
string
required
Notification body text
data
Record<string, string>
Optional data payload for deep linking and custom handling
priority
'high' | 'normal'
Notification priority (default: “normal”)
const result = await convex.action(api.shared.notifications.sendNotificationToUser, {
  userId: "user-123",
  title: "🍕 Order Ready!",
  body: "Your order #12345 is ready for pickup",
  data: {
    type: "order_status",
    orderId: "order-12345",
    deepLink: "twigz://orders/order-12345"
  },
  priority: "high"
});
{
  "success": true,
  "message": "Sent to 2/2 devices via FCM/APNs"
}

Schedule Notification

Schedule a notification to be sent at a specific future time.
userId
string
required
User ID to send to
title
string
required
Notification title
body
string
required
Notification body
scheduledTime
number
required
Unix timestamp for when to send
data
Record<string, string>
Optional data payload
priority
'high' | 'normal'
Notification priority
const notificationId = await convex.mutation(api.shared.notifications.scheduleNotification, {
  userId: "user-123",
  title: "⏰ Appointment Reminder",
  body: "Don't forget your appointment tomorrow at 3 PM",
  scheduledTime: Date.now() + (24 * 60 * 60 * 1000), // 24 hours from now
  data: {
    type: "reminder",
    appointmentId: "apt-456"
  },
  priority: "normal"
});
"sn123456789"

Cancel Scheduled Notification

Cancel a pending scheduled notification.
notificationId
Id<'scheduledNotifications'>
required
Scheduled notification ID to cancel
const cancelled = await convex.mutation(api.shared.notifications.cancelScheduledNotification, {
  notificationId: "sn123456789"
});
true

Send Bulk Notification

Send notifications to multiple users efficiently with batching.
userIds
Array<string>
required
Array of user IDs to send to
title
string
required
Notification title
body
string
required
Notification body
data
Record<string, string>
Optional data payload
batchSize
number
Batch size for processing (default: 50)
const result = await convex.action(api.shared.notifications.sendBulkNotification, {
  userIds: ["user-1", "user-2", "user-3"],
  title: "🎉 System Update",
  body: "New features are now available in the app!",
  data: {
    type: "announcement",
    updateVersion: "2.1.0"
  },
  batchSize: 100
});
{
  "totalUsers": 3,
  "successfulUsers": 3,
  "failedUsers": 0,
  "batchResults": [
    {
      "userId": "user-1",
      "success": true
    },
    {
      "userId": "user-2", 
      "success": true
    },
    {
      "userId": "user-3",
      "success": true
    }
  ]
}

Get User Device Tokens

Retrieve all active device tokens for a user.
userId
string
User ID (defaults to authenticated user)
const tokens = await convex.query(api.shared.notifications.getUserDeviceTokens, {
  userId: "user-123"
});
[
  {
    "_id": "dt123456789",
    "deviceToken": "fGHI...xyz",
    "platform": "android",
    "appVersion": "1.0.0",
    "isActive": true,
    "lastUpdated": 1640995200000
  },
  {
    "_id": "dt987654321",
    "deviceToken": "aBcD...123",
    "platform": "ios",
    "appVersion": "1.0.0",
    "isActive": true,
    "lastUpdated": 1640995800000
  }
]

Send Test Notification

Send a test notification with debugging information.
userId
string
required
User ID to send test to
title
string
Custom title (default: ”🧪 Test Notification”)
body
string
Custom body (default: timestamp)
const result = await convex.action(api.shared.notifications.sendTestNotification, {
  userId: "user-123",
  title: "🔥 FCM/APNs Test",
  body: "Testing direct Firebase/Apple notifications!"
});
{
  "success": true,
  "message": "Test notification sent successfully",
  "debugInfo": {
    "userHasTokens": true,
    "tokenCount": 2,
    "deliveryMethod": "FCM/APNs",
    "details": {
      "fcmTokens": 1,
      "apnsTokens": 1,
      "sentToFCM": 1,
      "sentToAPNs": 1
    }
  }
}

Debug User Notifications

Debug notification setup for troubleshooting.
userId
string
required
User ID to debug
const debug = await convex.query(api.shared.notifications.debugUserNotifications, {
  userId: "user-123"
});
{
  "userId": "user-123",
  "hasExpoPushToken": false,
  "hasNativeTokens": true,
  "deviceTokens": [
    {
      "_id": "dt123456789",
      "platform": "android",
      "isActive": true,
      "lastUpdated": 1640995200000
    }
  ],
  "recentNotifications": [
    {
      "_id": "n123456789",
      "title": "Test Notification",
      "sentAt": 1640995200000,
      "status": "delivered"
    }
  ],
  "troubleshooting": [
    "✅ User has active FCM/APNs tokens",
    "✅ Recent notifications delivered successfully",
    "💡 All systems operational"
  ]
}

Get Notification Statistics

Retrieve comprehensive notification statistics.
startDate
number
Filter start date (Unix timestamp)
endDate
number
Filter end date (Unix timestamp)
const stats = await convex.query(api.shared.notifications.getNotificationStats, {
  startDate: Date.now() - (7 * 24 * 60 * 60 * 1000), // Last 7 days
  endDate: Date.now()
});
{
  "totalScheduled": 150,
  "totalSent": 142,
  "totalFailed": 8,
  "totalPending": 0,
  "deviceTokenCount": {
    "ios": 45,
    "android": 67,
    "total": 112
  }
}

Notification Types

The system supports various notification types:

Order Status

Order updates, confirmations, and delivery notifications

Payment

Payment confirmations, failures, and refund notifications

Delivery

Delivery tracking, ETA updates, and completion notifications

Promotions

Marketing campaigns, discounts, and special offers

Announcements

System updates, maintenance notices, and feature releases

Store Status

Store approval, rejection, and operational status changes

Complete Integration Example

Here’s a complete notification integration:
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import { useMutation, useAction } from 'convex/react';
import { api } from './convex/_generated/api';

function useNotifications(userId: string) {
  const recordToken = useMutation(api.shared.notifications.recordNativeDeviceToken);
  const sendTest = useAction(api.shared.notifications.sendTestNotification);

  useEffect(() => {
    setupNotifications();
  }, [userId]);

  const setupNotifications = async () => {
    try {
      // Request permissions
      const { status } = await Notifications.requestPermissionsAsync();
      if (status !== 'granted') {
        console.warn('Notification permissions not granted');
        return;
      }

      // Get native device token (FCM/APNs)
      const tokenData = await Notifications.getDevicePushTokenAsync();
      
      // Register with backend
      await recordToken({
        deviceToken: tokenData.data,
        platform: Platform.OS === 'ios' ? 'ios' : 'android',
        appVersion: '1.0.0',
        deviceInfo: {
          deviceId: 'unique-device-id',
          deviceName: Platform.OS === 'ios' ? 'iPhone' : 'Android Device',
          osVersion: Platform.Version.toString()
        }
      });

      console.log('✅ Notification token registered successfully');

    } catch (error) {
      console.error('❌ Failed to setup notifications:', error);
    }
  };

  const testNotification = async () => {
    try {
      const result = await sendTest({
        userId,
        title: '🧪 Test from App',
        body: 'This is a test notification!'
      });
      console.log('Test result:', result);
    } catch (error) {
      console.error('Test failed:', error);
    }
  };

  // Handle received notifications
  useEffect(() => {
    const subscription = Notifications.addNotificationReceivedListener(notification => {
      console.log('📱 Notification received:', notification);
      
      // Handle notification data
      const data = notification.request.content.data;
      if (data?.type === 'order_status' && data?.orderId) {
        // Navigate to order details
        // navigation.navigate('Order', { orderId: data.orderId });
      }
    });

    return () => subscription.remove();
  }, []);

  return { testNotification };
}

export default useNotifications;

Environment Setup

To use FCM/APNs notifications, configure these environment variables:
Required environment variables:
FCM_SERVICE_ACCOUNT={"type":"service_account",...} # Firebase service account JSON
FCM_PROJECT_ID=your-firebase-project-id
Setup steps:
  1. Go to Firebase Console → Project Settings
  2. Generate a new private key for service account
  3. Copy the JSON content to FCM_SERVICE_ACCOUNT
  4. Set your project ID in FCM_PROJECT_ID
Required environment variables:
APNS_KEY_ID=ABC123DEF4 # APNs key ID
APNS_TEAM_ID=DEF456GHI7 # Apple Team ID  
APNS_PRIVATE_KEY=-----BEGIN PRIVATE KEY----- # APNs private key (.p8 file content)
IOS_BUNDLE_ID=com.yourapp.bundleid # iOS app bundle identifier
APNS_ENVIRONMENT=development # or "production"
Setup steps:
  1. Go to Apple Developer → Keys
  2. Create a new APNs key
  3. Download the .p8 file and copy content to APNS_PRIVATE_KEY
  4. Set the key ID, team ID, and bundle ID

Error Handling

Status Code: 400
{
  "error": "Invalid device token format",
  "platform": "android",
  "tokenLength": 152
}
Status Code: 404
{
  "error": "No active device tokens found for user",
  "userId": "user-123"
}
Status Code: 500
{
  "error": "Notification delivery failed",
  "details": {
    "fcmErrors": ["Invalid token"],
    "apnsErrors": ["Device token expired"]
  }
}
This notification system is production-ready and handles millions of notifications daily. It includes automatic retry logic, token validation, and comprehensive error handling.
Always test notifications in both development and production environments, as token formats and delivery behavior can differ between environments.