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.
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.
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 Response
No Tokens Response
{
"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.
The FCM device token to send to
Custom notification title (default: “Direct FCM Test”)
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 Response
Failure Response
{
"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" : "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
Array of FCM device tokens to send to
Optional key-value data payload
{
"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
Array of APNs device tokens to send to
Optional key-value data payload (merged into the APNs payload alongside aps)
App badge count to display
Notification sound name (default: “default”)
{
"success" : true ,
"results" : [
{
"deviceToken" : "a1b2c3d4e5f6..." ,
"success" : true
},
{
"deviceToken" : "f6e5d4c3b2a1..." ,
"success" : false ,
"error" : "BadDeviceToken"
}
]
}