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.

Debug & Testing

These functions live in convex/shared/notificationDebug.ts and convex/shared/notificationActions.ts. They provide low-level debugging, direct platform testing, and internal send capabilities for the notification system.

debugNotificationSystem

Type: query Comprehensive health check for a user’s notification setup. Returns device tokens, recent notification history, environment configuration status, and actionable recommendations.
userId
string
required
The user ID to inspect
const debug = await convex.query(api.shared.notificationDebug.debugNotificationSystem, {
  userId: "user-123"
});

console.log("Tokens:", debug.deviceTokenCount);
console.log("FCM configured:", debug.environmentCheck.hasFCMConfig);
console.log("APNs configured:", debug.environmentCheck.hasAPNSConfig);
console.log("Recommendations:", debug.recommendations);
{
  "userId": "user-123",
  "hasDeviceTokens": true,
  "deviceTokenCount": 2,
  "tokens": [
    {
      "deviceToken": "fGHIjklmnop1234567890abcde...",
      "platform": "android",
      "isActive": true,
      "lastUpdated": 1640995200000,
      "deviceId": "device-abc",
      "failedDeliveryCount": 0
    }
  ],
  "recentNotifications": [
    {
      "title": "Order Ready!",
      "status": "sent",
      "createdAt": 1640995200000
    }
  ],
  "environmentCheck": {
    "hasFCMConfig": true,
    "hasAPNSConfig": true
  },
  "recommendations": [
    "Found 2 active device token(s)"
  ]
}

testSendNotification

Type: action (requires authentication) Sends a test notification to the currently authenticated user across all their registered devices. Useful for verifying end-to-end notification delivery without needing to specify a user ID.
This function requires authentication. It automatically sends to the calling user’s devices.
const result = await convex.action(api.shared.notificationDebug.testSendNotification, {});

if (result.success) {
  console.log("Test notification sent!", result.message);
} else {
  console.error("Failed:", result.message, result.details);
}
{
  "success": true,
  "message": "Sent to 2/2 devices via FCM/APNs",
  "details": {
    "sentCount": 2,
    "failedCount": 0,
    "platforms": [
      { "platform": "android", "success": true },
      { "platform": "ios", "success": true }
    ]
  }
}

testFCMDirect

Type: action (requires authentication) Manually test FCM delivery by sending a notification to a specific FCM device token. Bypasses user lookup and sends directly to the provided token. Useful for isolating whether an FCM token is valid and reachable.
fcmToken
string
required
The FCM device token to send to
title
string
Custom notification title (default: “Direct FCM Test”)
body
string
Custom notification body (default: “This is a direct FCM test notification”)
const result = await convex.action(api.shared.notificationDebug.testFCMDirect, {
  fcmToken: "dGVzdC10b2tlbi0xMjM0NTY3ODkw...",
  title: "Direct Token Test",
  body: "Testing this specific FCM token"
});

console.log(result.success ? "Token is valid!" : "Token failed:", result.message);
{
  "success": true,
  "message": "FCM notification sent successfully",
  "details": {
    "success": true,
    "results": [
      {
        "deviceToken": "dGVzdC10b2tlbi0xMjM0NTY3ODkw...",
        "success": true
      }
    ]
  }
}

debugNotificationSystemInternal

Type: query (internal)
This is an internal function intended for use by other server-side actions. It is not directly callable from client code.
Lightweight internal version of debugNotificationSystem. Returns a minimal summary of a user’s device token status for use in server-side logic and other actions.
userId
string
required
The user ID to check
{
  "userId": "user-123",
  "hasDeviceTokens": true,
  "deviceTokenCount": 2
}

sendFCMNotificationDirect

Type: internalAction (Node.js runtime)
This is an internal action that runs in the Node.js runtime. It is not directly callable from client code. It is used internally by testFCMDirect and the main notification dispatch system.
Sends push notifications to one or more Android devices via the Firebase Cloud Messaging (FCM) v1 API. Handles OAuth 2.0 token exchange using the configured service account and sends to each device token individually. Requires environment variables: FCM_SERVICE_ACCOUNT, FCM_PROJECT_ID
deviceTokens
Array<string>
required
Array of FCM device tokens to send to
title
string
required
Notification title
body
string
required
Notification body text
data
Record<string, string>
Optional key-value data payload
priority
'high' | 'normal'
FCM delivery priority
{
  "success": true,
  "results": [
    {
      "deviceToken": "fGHI...xyz",
      "success": true
    },
    {
      "deviceToken": "aBcD...123",
      "success": false,
      "error": "Requested entity was not found."
    }
  ]
}

sendAPNsNotificationDirect

Type: internalAction (Node.js runtime)
This is an internal action that runs in the Node.js runtime. It is not directly callable from client code. It is used internally by the main notification dispatch system for iOS delivery.
Sends push notifications to one or more iOS devices via the Apple Push Notification service (APNs). Generates a JWT for APNs authentication and sends to each device token individually. Automatically selects the sandbox or production APNs endpoint based on the APNS_ENVIRONMENT variable. Requires environment variables: APNS_KEY_ID, APNS_TEAM_ID, APNS_PRIVATE_KEY, IOS_BUNDLE_ID, APNS_ENVIRONMENT
deviceTokens
Array<string>
required
Array of APNs device tokens to send to
title
string
required
Notification title
body
string
required
Notification body text
data
Record<string, string>
Optional key-value data payload (merged into the APNs payload alongside aps)
badge
number
App badge count to display
sound
string
Notification sound name (default: “default”)
{
  "success": true,
  "results": [
    {
      "deviceToken": "a1b2c3d4e5f6...",
      "success": true
    },
    {
      "deviceToken": "f6e5d4c3b2a1...",
      "success": false,
      "error": "BadDeviceToken"
    }
  ]
}