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.
Native FCM token (Android) or APNs token (iOS)
platform
'ios' | 'android'
required
Device platform
Device information object with deviceId, deviceName, and osVersion
Expo Integration
React Native
JavaScript
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"
}
});
Send Notification to User ⭐
PRIMARY FUNCTION - Sends notifications to a user across all their devices via FCM/APNs.
User ID to send notification to
Optional data payload for deep linking and custom handling
Notification priority (default: “normal”)
TypeScript
JavaScript
Python
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.
Unix timestamp for when to send
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"
});
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"
});
Send Bulk Notification
Send notifications to multiple users efficiently with batching.
Array of user IDs to send to
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.
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.
Custom title (default: ”🧪 Test Notification”)
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.
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.
Filter start date (Unix timestamp)
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:
React Hook
Vue Composition API
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:
Go to Firebase Console → Project Settings
Generate a new private key for service account
Copy the JSON content to FCM_SERVICE_ACCOUNT
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:
Go to Apple Developer → Keys
Create a new APNs key
Download the .p8 file and copy content to APNS_PRIVATE_KEY
Set the key ID, team ID, and bundle ID
Error Handling
Token registration failed
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.