Integrating Salesforce with Firebase Cloud Messaging (FCM)
Mobile functionality is a critical component of modern CRM. This article outlines a metadata-driven architecture to integrate Salesforce with Firebase Cloud Messaging (FCM) for robust mobile push notifications. We will cover device registration, topic subscription management, and real-time notification delivery across Sales, Service, and Field Service clouds using Apex and Google OAuth 2.0.
Instant notifications are essential for business responsiveness, whether a sales representative receives a lead alert or a field service technician is notified of a dispatch change. While Salesforce offers native tools, custom mobile applications often require a dedicated integration solution.
This guide provides a detailed walkthrough of a scalable, secure, and deployment-friendly integration strategy leveraging Custom Metadata (CMDT).
Architecture and Strategy
The core of this solution is a decoupled architecture separating Device Management from Message Delivery. By utilizing FCM Topics, the burden of managing individual device tokens is offloaded to Firebase, allowing Salesforce to function efficiently as the system of record.
The Four-Stage Lifecycle
- Handshake: The mobile application captures an FCM Token upon user login and stores it within Salesforce.
- Subscription: Salesforce invokes the Firebase
batchAddAPI to associate the token with a specific FCM Topic (e.g.,Field_Service_Alerts). - Metadata Engine: All API credentials and endpoints are stored in CMDT for environment-specific configuration and agility.
- Delivery: A business event triggers a synchronous Apex callout to the FCM API for notification dispatch.
Firebase Cloud Messaging (FCM) is chosen for its capability to simultaneously deliver messages to both Android and iOS devices. This project specifically employs the FCM HTTP v1 API, which enhances security through the use of short-lived access tokens.
To establish a secure connection between Salesforce and Google, an Azure Function serves as a secure middleware. This function stores sensitive private keys, preventing their exposure within the Salesforce org. Additionally, the Azure Function handles complex data processing, mitigating the risk of exhausting Salesforce governor limits.
By leveraging FCM Topics, Salesforce only needs to send a single message to a group, rather than managing thousands of individual alerts. This decoupled approach ensures Salesforce remains performant and responsive.
This strategy establishes an enterprise-grade security perimeter that is easily maintainable.
Integration Flow: A Detailed Walkthrough
The integration process follows a specific sequence to ensure security and reliability:
- Device Registration: Upon login, the mobile device captures its unique FCM token and registers it with Salesforce.
- Authentication Handshake: Salesforce initiates an authentication token request to the Azure Function. The Azure Function validates the request and returns an authentication token to Salesforce.
- Topic Subscription: Salesforce then sends a subscription request to the Azure Function. Acting as the secure middleman, the Azure Function utilizes Firebase Admin SDK and Azure Key Vault to securely communicate with Firebase.
- Confirmation and Error Handling: Firebase sends a success or failure status back to the Azure Function, which relays it to Salesforce. In case of subscription failure, Salesforce is configured to automatically send an email notification to the business analyst and developer for prompt troubleshooting.
- Push Delivery: Once the handshake is complete, Salesforce sends notification requests directly to Firebase via the FCM API. Firebase then pushes the notifications to the target mobile devices.
Firebase and Azure Function Setup
Prior to implementing Apex code, configure your environment in Google Cloud and Azure:
- Firebase Project Setup: Create a project in the Firebase Console. Ensure the Firebase Cloud Messaging API (V1) is enabled under
Project Settings > Cloud Messaging. - Service Account Key: Generate a Service Account JSON key from the Service Accounts tab. This key contains the
private_keyandclient_emailrequired for authentication. - Azure Function Middleware: Set up an Azure Function to act as your secure middleware. In the Azure Portal, create a Function App and store your Firebase credentials within Environment Variables (Application Settings) for maximum security. Obtain your Function URL and Function Key for Salesforce callouts.
Refer to the Official Firebase Setup Guide and Azure Functions Quickstart for detailed instructions.
Solution: Configuration and Implementation
1. The Configuration Layer
Two separate Custom Metadata types are used:
Azure_Firebase_Creds__mdt: For storing Azure server details.- Fields:
Client_ID__c,Client_Secret__c,Grant_Type__c,Scope__c,Subscribe_to_Topic_URL__c,Token_URL__c
- Fields:
FirebaseCreds__mdt: For storing Firebase credentials.- Fields:
Aud__c,Client_Email__c,Endpoint__c,Private_Key__c,Private_Key_Id__c,PushNotificationUrl__c,Scope__c
- Fields:
Remote Site Settings: Ensure the following endpoints are configured:
https://login.microsoftonline.com(if using Azure AD authentication)https://oauth2.googleapis.comhttps://fcm.googleapis.com
2. Device Subscription (Apex)
Key Implementation Details:
- Asynchronous Processing and UI Responsiveness: The
@future(callout=true)annotation is used to decouple external callouts from the Salesforce transaction, preventingCallout from Triggerexceptions and maintaining UI responsiveness. - Governor-Resilient Bulkification: A "Collect-then-Commit" pattern is employed. By processing a
Set<Id>and deferring allupdateandinsertoperations outside the loop, the code can handle hundreds of simultaneous device updates without violating Apex DML limits. - Environment Agility and Metadata Security: Custom Metadata and
OrganizationIdchecks are used instead of hardcoded secrets. This allows the code to dynamically adjust endpoints and Topic names for UAT or Production environments, simplifying deployments. - Fail-Safe Logging: Each transaction logs an activity record in the
Mobile_Application_API_Activity__ccustom object, providing an audit trail for troubleshooting authentication and connection issues.
The Implementation:
When a user logs in or updates permissions in the mobile app, a record is created or updated in the Mobile_Device_Data__c object, triggering the following subscription logic:
public class SubscribeToFCMHelper {
public static String getAccessToken() {
Azure_Firebase_Creds__mdt creds = Azure_Firebase_Creds__mdt.getInstance('firebase');
if (creds == null) throw new CalloutException('Firebase credentials metadata not found.');
HttpRequest req = new HttpRequest();
req.setEndpoint(creds.Token_URL__c);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
String requestBody = 'client_id=' + EncodingUtil.urlEncode(creds.Client_ID__c, 'UTF-8') +
'&scope=' + EncodingUtil.urlEncode(creds.Scope__c, 'UTF-8') +
'&grant_type=' + EncodingUtil.urlEncode(creds.Grant_Type__c, 'UTF-8') +
'&client_secret=' + EncodingUtil.urlEncode(creds.Client_Secret__c, 'UTF-8');
req.setBody(requestBody);
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) responseBody.get('access_token');
} else {
throw new CalloutException('Failed to retrieve Access Token: ' + res.getBody());
}
}
/**
* Future method for handling Firebase Topic Subscriptions.
* Best Practice: Bulkified DML and Error Tracking.
*/
@future(callout=true)
public static void subscribe(Set<Id> deviceIds) {
List<Mobile_Device_Data__c> devicesToUpdate = [SELECT Id, User__c, Device_Token_Fcm__c, H_Application_Name__c
FROM Mobile_Device_Data__c
WHERE Id IN :deviceIds];
if (devicesToUpdate.isEmpty()) return;
String accessToken = getAccessToken();
Azure_Firebase_Creds__mdt creds = Azure_Firebase_Creds__mdt.getInstance('firebase');
List<Mobile_Application_API_Activity__c> logs = new List<Mobile_Application_API_Activity__c>();
for (Mobile_Device_Data__c detail : devicesToUpdate) {
if (String.isBlank(detail.Device_Token_Fcm__c)) continue;
// Logic to determine project key
String projectKey = detail.H_Application_Name__c != null && detail.H_Application_Name__c.containsIgnoreCase('APP Name')
? Label.FIREBASE_CONFIG_COREAPP
: '';
String appName = detail.H_Application_Name__c != null ? detail.H_Application_Name__c.deleteWhitespace() : 'UnknownApp';
if(UserInfo.getOrganizationId()==System.Label.UAT_ORG_ID) appname=appname+'UAT';
// Use Map for JSON Body to avoid manual concatenation errors
Map<String, Object> jsonMap = new Map<String, Object>{
'project_key' => projectKey,
'registration_tokens' => new List<String>{ detail.Device_Token_Fcm__c },
'topic' => appName
};
String jsonBody = JSON.serialize(jsonMap);
try {
HttpRequest req = new HttpRequest();
req.setEndpoint(creds.Subscribe_to_Topic_URL__c);
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + accessToken);
req.setBody(jsonBody);
HttpResponse res = new Http().send(req);
String responseBody = res.getBody();
Boolean isSuccess = (res.getStatusCode() == 200);
// Update device record on success
if (isSuccess) {
detail.Subscribed_to_Notification__c = true;
detail.Last_Subscribe_Time__c = System.now();
}
// Create log record - Collect in list for bulk DML
logs.add(new Mobile_Application_API_Activity__c(
Command_Type__c = 'SUBSCRIBE TO FCM',
Request_Body__c = jsonBody,
Response_Body__c = responseBody.left(131072), // Ensure it fits in Long Text area
Result__c = isSuccess ? 'PASS' : 'FAIL',
Is_Fail__c = !isSuccess,
Error__c = isSuccess ? '' : responseBody.left(255),
Record_Identifier__c = detail.Id,
User__c = detail.User__c
));
} catch (Exception e) {
logs.add(new Mobile_Application_API_Activity__c(
Command_Type__c = 'SUBSCRIBE TO FCM ERROR',
Result__c = 'FAIL',
Is_Fail__c = true,
Error__c = e.getMessage().left(255),
Record_Identifier__c = detail.Id,
User__c = detail.User__c
));
}
}
// Bulk DML for device updates and logs
if (!devicesToUpdate.isEmpty()) {
try {
update devicesToUpdate;
} catch (DmlException e) {
// Log DML errors if necessary
}
}
if (!logs.isEmpty()) {
try {
insert logs;
} catch (DmlException e) {
// Log DML errors if necessary
}
}
}
}
Key Takeaways
- A decoupled architecture using FCM Topics and a secure middleware (Azure Function) is crucial for scalability and performance.
- Custom Metadata (CMDT) enables environment-specific configurations and secure credential management.
- Asynchronous Apex (
@future) and bulkified DML operations are essential for handling callouts and avoiding governor limit issues. - Comprehensive logging in a custom object (
Mobile_Application_API_Activity__c) provides an audit trail for troubleshooting.
Leave a Comment