diff --git a/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js b/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
--- a/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
+++ b/keyserver/flow-typed/npm/@parse/node-apn_vx.x.x.js
@@ -38,7 +38,7 @@
     // be accessed from apn.Notification instance
     aps: {
       +badge: string | number,
-      +alert: string,
+      +alert: string | { +body?: string, ... },
       +'thread-id': string,
       +'mutable-content': boolean,
       +sound: string,
diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
--- a/keyserver/src/push/crypto.js
+++ b/keyserver/src/push/crypto.js
@@ -4,6 +4,8 @@
 import invariant from 'invariant';
 import _cloneDeep from 'lodash/fp/cloneDeep.js';
 
+import { NEXT_CODE_VERSION } from 'lib/shared/version-utils.js';
+
 import type {
   AndroidNotification,
   AndroidNotificationPayload,
@@ -15,6 +17,7 @@
 async function encryptIOSNotification(
   cookieID: string,
   notification: apn.Notification,
+  codeVersion?: ?number,
   notificationSizeValidator?: apn.Notification => boolean,
 ): Promise<{ +notification: apn.Notification, +payloadSizeExceeded: boolean }> {
   invariant(
@@ -62,6 +65,18 @@
     );
 
     encryptedNotification.payload.encryptedPayload = serializedPayload.body;
+
+    if (
+      codeVersion &&
+      codeVersion >= NEXT_CODE_VERSION &&
+      codeVersion % 2 === 0
+    ) {
+      encryptedNotification.aps = {
+        alert: { body: 'ENCRYPTED' },
+        ...encryptedNotification.aps,
+      };
+    }
+
     return {
       notification: encryptedNotification,
       payloadSizeExceeded: !!dbPersistConditionViolated,
@@ -200,6 +215,7 @@
 function prepareEncryptedIOSNotifications(
   devices: $ReadOnlyArray<NotificationTargetDevice>,
   notification: apn.Notification,
+  codeVersion?: ?number,
   notificationSizeValidator?: apn.Notification => boolean,
 ): Promise<
   $ReadOnlyArray<{
@@ -214,6 +230,7 @@
       const notif = await encryptIOSNotification(
         cookieID,
         notification,
+        codeVersion,
         notificationSizeValidator,
       );
       return { cookieID, deviceToken, ...notif };
@@ -225,6 +242,7 @@
 function prepareEncryptedIOSNotificationRescind(
   devices: $ReadOnlyArray<NotificationTargetDevice>,
   notification: apn.Notification,
+  codeVersion?: ?number,
 ): Promise<
   $ReadOnlyArray<{
     +cookieID: string,
@@ -237,6 +255,7 @@
       const { notification: notif } = await encryptIOSNotification(
         cookieID,
         notification,
+        codeVersion,
       );
       return { deviceToken, cookieID, notification: notif };
     },
diff --git a/keyserver/src/push/rescind.js b/keyserver/src/push/rescind.js
--- a/keyserver/src/push/rescind.js
+++ b/keyserver/src/push/rescind.js
@@ -247,6 +247,7 @@
   encryptCallback: (
     devices: $ReadOnlyArray<NotificationTargetDevice>,
     notification: T,
+    codeVersion?: ?number,
   ) => Promise<
     $ReadOnlyArray<{
       +notification: T,
@@ -262,7 +263,11 @@
       deviceToken,
     }));
   }
-  const notifications = await encryptCallback(devices, notification);
+  const notifications = await encryptCallback(
+    devices,
+    notification,
+    codeVersion,
+  );
   return notifications.map(({ deviceToken, notification: notif }) => ({
     deviceToken,
     notification: notif,
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -784,6 +784,7 @@
   const notifsWithMessageInfos = await prepareEncryptedIOSNotifications(
     devices,
     copyWithMessageInfos,
+    platformDetails.codeVersion,
     notificationSizeValidator,
   );
 
@@ -803,6 +804,7 @@
   const notifsWithoutMessageInfos = await prepareEncryptedIOSNotifications(
     devicesWithExcessiveSize,
     notification,
+    platformDetails.codeVersion,
   );
 
   const targetedNotifsWithMessageInfos = notifsWithMessageInfos
@@ -1354,6 +1356,7 @@
           const notificationsArray = await prepareEncryptedIOSNotifications(
             deviceInfos,
             notification,
+            codeVersion,
           );
           targetedNotifications = notificationsArray.map(
             ({ notification: notif, deviceToken }) => ({
diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm
--- a/native/ios/NotificationService/NotificationService.mm
+++ b/native/ios/NotificationService/NotificationService.mm
@@ -58,6 +58,15 @@
     self.contentHandler([[UNNotificationContent alloc] init]);
     return;
   }
+
+  if ([self isBadgeOnly:self.bestAttemptContent.userInfo]) {
+    UNMutableNotificationContent *badgeOnlyContent =
+        [[UNMutableNotificationContent alloc] init];
+    badgeOnlyContent.badge = self.bestAttemptContent.badge;
+    self.contentHandler(badgeOnlyContent);
+    return;
+  }
+
   [self sendNewMessageInfosNotification];
   // TODO modify self.bestAttemptContent here
 
@@ -155,6 +164,12 @@
       [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"];
 }
 
+- (BOOL)isBadgeOnly:(NSDictionary *)payload {
+  // TODO: refactor this check by introducing
+  // badgeOnly property in iOS notification payload
+  return !payload[@"threadID"];
+}
+
 - (void)sendNewMessageInfosNotification {
   CFNotificationCenterPostNotification(
       CFNotificationCenterGetDarwinNotifyCenter(),