Imagine a field representative updating an Interaction from a lightning record page on their laptop just before heading out. The update saves to Salesforce, but the iPad app they use in the car is offline-first and still shows stale data. The rep opens the app, expects to see their latest changes, and instead sees yesterday’s state. They now have to run a full sync, which can be slow and bandwidth-heavy, just to retrieve a single updated record.
This is the real problem this article aims to address.
Traditional Salesforce Mobile SDK apps rely heavily on pull-based sync: the device periodically calls REST APIs and applies filters (territory, briefcase, etc.) to refresh local data. This works, but it means:
- The device doesn’t know when something changed in Salesforce.
- You either poll aggressively (wasting battery and bandwidth), or accept stale data until the next sync.
In this article, we will:
- Look at how Salesforce push notifications and the Mobile SDK fit together.
- Show how to configure a connected app and iOS client to receive push notifications.
- Demonstrate how to send targeted push notifications to specific users from Apex.
- Show how to handle silent push notifications in the mobile app to enable near-real-time, selective sync.
- Conclude with design trade-offs and when this pattern is appropriate.
This article focuses on behavior and design, not on every detail of every configuration screen.
Two Ways to Use Push in a Mobile App
From a mobile client’s point of view, Salesforce push notifications are just APNs notifications with a JSON payload. How you use them depends on the behavior you want.
Visible Notifications (Alerts, Banners, Sounds)
The default pattern people think of is a visible notification:
- APNs payload includes an aps.alert and possibly a sound.
- iOS shows a banner, plays a sound, and increments the badge count.
- The app can handle taps to navigate to a screen.
This is ideal for:
- Approvals and tasks (“You have a new approval request”).
- Time-critical alerts (“Appointment starting soon”).
It is not ideal when:
- You have many record changes.
- You only care about refreshing data silently, not interrupting the user.
Silent Notifications (content-available)
A silent notification is one that:
- Uses
aps.content-available = 1. - Does not specify an alert, sound, or badge.
- Wake your app in the background so it can run code (briefly) without showing UI.
For an offline-first Salesforce app, this is the interesting case:
- Salesforce sends a silent push saying “record X changed”.
- iOS wakes the app.
- The app fetches just record X from Salesforce and updates its local store.
- The user sees up-to-date data the next time they open the app, without having seen any banners.
In the rest of the article, we focus on this pattern: using silent push as a sideband trigger for selective sync.
Step 1: Configure Push on the Salesforce Side
To send push notifications from Salesforce to a Mobile SDK app, you need:
- A Connected App representing the iOS app.
- APNs credentials for that app (Auth Key or certificate).
- Push is enabled on the connected app.
At a high level:
In the Apple Developer portal:
- Create an App ID with your bundle identifier (e.g.
com.company.agent.ipad). - Enable Push Notifications for that App ID.
- Create an APNs Auth Key (
.p8) with APNs enabled. - Note the Key ID and Team ID.
In Salesforce Setup:
- Open your Connected App (used by the Mobile SDK).
- In the iOS push configuration:
-- Upload the
.p8as Signing Key. -- Enter the Key Identifier and Team Identifier. -- Set Bundle ID to match the iOS app’s bundle id exactly. -- Choose the correct environment (Sandbox for dev builds).
Save the connected app and verify there are no errors.
Once configured correctly, Salesforce can call APNs on behalf of this app. If you saw errors like “provider token is not valid” or “pushing to this topic is not allowed” during setup, they almost always traced back to mismatched Team ID / bundle id / APNs key.
Step 2: Register Devices with the Salesforce Mobile SDK
When you run the iOS app on a real device, the app must:
- Ask the user for notification permission.
- Register with APNs to obtain a device token.
- Register that token with Salesforce through the Mobile SDK.
A simplified pattern looks like this:
import UserNotifications
import SalesforceSDKCore
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
registerForRemotePushNotifications()
return true
}
private func registerForRemotePushNotifications() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
} else {
// Log or handle denial
}
}
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
// Forward the token to Salesforce Mobile SDK
SFPushNotificationManager.sharedInstance()
.didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
}
}
Once this runs after a successful Mobile SDK login, Salesforce creates a MobilePushServiceDevice record for that user + connected app + device. You can verify this with a simple SOQL query, as you already do in your setup docs.
Step 3: Send a Test Push to a Specific User
With the connected app and registration in place, you can send a targeted push from Apex by specifying:
- The connected app name.
- The user Id (or multiple user Ids).
- The JSON payload.
A basic visible test (used during setup) looked like:
String appName = 'Real_time_Sync'; // Connected App API name
String userId = 'USER_SALESFORCE_ID'; // Test user Id
Messaging.PushNotification msg = new Messaging.PushNotification();
Map<String, Object> payload = Messaging.PushNotificationPayload.apple(
'Test Push Notification',
'default',
1,
new Map<String, Object>{ 'type' => 'TEST', 'message' => 'Hello from Salesforce' }
);
msg.setPayload(payload);
Set<String> users = new Set<String>{ userId };
try {
msg.send(appName, users);
System.debug('Push notification sent successfully!');
} catch (Exception e) {
System.debug('Push failed: ' + e.getMessage());
}
This is useful to confirm that:
- APNs' credentials are valid.
- Device is registered.
- The app is receiving notifications and logging them. Once that works, you can switch to silent notifications.
Step 4: Send Silent, Data-Oriented Push Notifications
To avoid spamming the user, you convert the payload to a silent notification:
- No
alert. - No
sound. -
aps.content-available = 1. - Custom keys at the root for your app (e.g. type, recordIds). A simplified example for Account updates:
trigger accountAlert on Account (after update) {
final String TARGET_USER_ID = 'TEST_USER_ID'; // Example user
for (Account acc: Trigger.New) {
Account oldAcc = Trigger.oldMap.get(acc.Id);
Boolean nameChanged = acc.Name != oldAcc.Name;
Boolean ownerChanged = acc.OwnerId != oldAcc.OwnerId;
Boolean typeChanged = acc.Type != oldAcc.Type;
if (nameChanged || ownerChanged || typeChanged) {
List<String> changes = new List<String>();
if (nameChanged) changes.add('Name');
if (ownerChanged) changes.add('Owner');
if (typeChanged) changes.add('Type');
String changedFields = String.join(changes, ', ');
// APS for silent push
Map<String, Object> aps = new Map<String, Object>{
'content-available' => 1
};
// Full payload: aps + custom data
Map<String, Object> payload = new Map<String, Object>();
payload.put('aps', aps);
payload.put('accountId', acc.Id);
payload.put('accountName', acc.Name);
payload.put('changedFields', changedFields);
payload.put('type', 'ACCOUNT_UPDATE');
Messaging.PushNotification msg = new Messaging.PushNotification();
msg.setPayload(payload);
Set<String> users = new Set<String>{ TARGET_USER_ID };
try {
msg.send('LSC_Real_time_Sync', users);
System.debug('Silent push notification sent for Account: ' + acc.Name);
} catch (Exception e) {
System.debug(LoggingLevel.ERROR,
'Silent push notification failed for Account ' + acc.Name +
': ' + e.getMessage());
}
}
}
}
You can apply the same pattern to Interactions or LMR-related objects, and further restrict to:
-
LastModifiedById == current userto avoid system-generated updates. - Excluding updates from sync queues or integration users. This keeps pushing focused on user-initiated changes.
Step 5: Handle Push Notifications in the iOS App
On the client side, you already have a unified handler to inspect incoming notification data.
At minimum:
-
Foreground:
userNotificationCenter(_:willPresent:withCompletionHandler:). -
Tap:
userNotificationCenter(_:didReceive:withCompletionHandler:). -
Background/silent:
application(_:didReceiveRemoteNotification:fetchCompletionHandler:). A simplified version that logs and reacts to custom keys:
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
handlePushNotification(userInfo: userInfo)
completionHandler(.newData)
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
handlePushNotification(userInfo: userInfo)
completionHandler([]) // no banner/sound for silent sync
}
private func handlePushNotification(userInfo: [AnyHashable: Any]) {
// Example: respond to account/interaction updates
if let type = userInfo["type"] as? String {
switch type {
case "ACCOUNT_UPDATE":
if let accountId = userInfo["accountId"] as? String {
// Trigger a targeted fetch for this account
refreshAccount(withId: accountId)
}
case "INTERACTION":
if let ids = userInfo["recordIds"] as? [String] {
refreshInteractions(withIds: ids)
}
default:
break
}
}
}
The refreshAccount / refreshInteractions functions can use SFRestAPI to call your existing, territory-aware sync endpoints and update your local database. The important part is that:
Push only carries IDs and type, not full record data.
The app decides how and when to fetch the rest.
Design Considerations and Trade-Offs
The approach led to some clear conclusions about how to use this pattern safely:
Use push as a sideband hint, not as the primary sync mechanism
- Push is best-effort and subject to APNs/iOS behavior.
- Regular sync remains the authoritative way to catch all changes.
Limit scope to user-initiated changes
- Generate pushes when the current user modifies records via Agentforce/LWC/web UI.
- Avoid pushing for backend/sync-queue changes to prevent noise and overload.
Keep payload small and focused
- Use push to send only
typeand a small set ofrecordIds. - Let the app fetch full records via REST/Composite or your existing sync APIs.
Accept possible duplicate downloads
- A record refreshed via push may come down again in the next regular sync because its
LastModifiedDateis newer than the last sync date. - With idempotent merge logic, this is a performance cost, not a correctness problem.
Prefer silent notifications for data refresh
- Visible notifications are for user awareness; silent notifications are for data state.
- For offline-first apps, silent push is usually the right default for “real-time updates”.
Recap
To summarize:
- Salesforce + Mobile SDK + APNs give you a way to target specific users with push notifications based on business events.
- For real-time data freshness in offline-first apps, silent push notifications are the right primitive: they wake the app, carry record IDs, and let the client fetch updated data.
- The full path:
-- Connected App with APNs credentials.
-- Device registration via SFPushNotificationManager.
-- Apex push using Messaging.PushNotification to a user.
-- iOS handlers logging and handling payloads, ready to trigger selective sync.
- By scoping push to user-initiated changes and treating it as a sideband channel, you get near real-time updates for critical objects without modifying the core sync algorithm or compromising data integrity.
References
Salesforce
- Salesforce Mobile Notifications Implementation Guide (PDF) https://resources.docs.salesforce.com/latest/latest/en-us/sfdc/pdf/salesforce_mobile_push_notifications_implementation.pdf
- Mobile Notifications Overview (HTML) https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_overview.htm
- Push Notifications and Mobile SDK https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-intro.html
- Enable Push Notifications in a Salesforce Mobile SDK iOS App https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_client_app_ios.htm
- Using Push Notifications in iOS (Mobile SDK) https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-using-ios.html
- Messaging.PushNotification Class (Apex Reference) https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_push_notification.htm
- Messaging.PushNotification Methods (send, etc.) https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_Messaging_PushNotification_methods.htm
- Using Apex Triggers to Send Push Notifications https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_apex_trigger.htm
Apple
- Generating a Remote Notification (payload format and 4 KB limit) https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification
- Creating the Remote Notification Payload (archived guide) https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html
- Payload Key Reference (JSON size and keys) https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html
- Technical Note TN2265 – Troubleshooting Push Notifications https://developer.apple.com/library/archive/technotes/tn2265/_index.html
- Sending Notification Requests to APNs https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns
- Troubleshooting Push Notifications (UserNotifications) https://developer.apple.com/documentation/usernotifications/troubleshooting-push-notifications
The end result is a Salesforce mobile app that feels much more responsive: important user-driven changes show up quickly, while the existing offline sync remains the safety net that keeps everything consistent.
Top comments (0)