diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js
--- a/lib/push/send-hooks.react.js
+++ b/lib/push/send-hooks.react.js
@@ -4,16 +4,22 @@
 
 import {
   preparePushNotifs,
-  type PerUserNotifBuildResult,
+  type PerUserTargetedNotifications,
 } from './send-utils.js';
 import { ENSCacheContext } from '../components/ens-cache-provider.react.js';
 import { NeynarClientContext } from '../components/neynar-client-provider.react.js';
 import type { MessageData } from '../types/message-types.js';
+import type {
+  EncryptedNotifUtilsAPI,
+  SenderDeviceDescriptor,
+} from '../types/notif-types.js';
 import { useSelector } from '../utils/redux-utils.js';
 
 function usePreparePushNotifs(): (
+  encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI,
+  senderDeviceDescriptor: SenderDeviceDescriptor,
   messageDatas: $ReadOnlyArray<MessageData>,
-) => Promise<?PerUserNotifBuildResult> {
+) => Promise<?PerUserTargetedNotifications> {
   const rawMessageInfos = useSelector(state => state.messageStore.messages);
   const rawThreadInfos = useSelector(state => state.threadStore.threadInfos);
   const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos);
@@ -23,8 +29,14 @@
   const getFCNames = React.useContext(NeynarClientContext)?.getFCNames;
 
   return React.useCallback(
-    (messageDatas: $ReadOnlyArray<MessageData>) => {
+    (
+      encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+      senderDeviceDescriptor: SenderDeviceDescriptor,
+      messageDatas: $ReadOnlyArray<MessageData>,
+    ) => {
       return preparePushNotifs({
+        encryptedNotifUtilsAPI,
+        senderDeviceDescriptor,
         messageInfos: rawMessageInfos,
         rawThreadInfos,
         auxUserInfos,
diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js
--- a/lib/push/send-utils.js
+++ b/lib/push/send-utils.js
@@ -3,11 +3,20 @@
 import _pickBy from 'lodash/fp/pickBy.js';
 import uuidv4 from 'uuid/v4.js';
 
-import { generateNotifUserInfoPromise } from './utils.js';
+import { createAndroidVisualNotification } from './android-notif-creators.js';
+import { createAPNsVisualNotification } from './apns-notif-creators.js';
+import {
+  stringToVersionKey,
+  getDevicesByPlatform,
+  generateNotifUserInfoPromise,
+} from './utils.js';
+import { createWebNotification } from './web-notif-creators.js';
+import { createWNSNotification } from './wns-notif-creators.js';
 import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js';
 import {
   rawMessageInfoFromMessageData,
   createMessageInfo,
+  shimUnsupportedRawMessageInfos,
 } from '../shared/message-utils.js';
 import { pushTypes } from '../shared/messages/message-spec.js';
 import { messageSpecs } from '../shared/messages/message-specs.js';
@@ -17,7 +26,7 @@
   threadInfoFromRawThreadInfo,
 } from '../shared/thread-utils.js';
 import type { AuxUserInfos } from '../types/aux-user-types.js';
-import type { PlatformDetails } from '../types/device-types.js';
+import type { PlatformDetails, Platform } from '../types/device-types.js';
 import {
   identityDeviceTypeToPlatform,
   type IdentityPlatformDetails,
@@ -28,7 +37,13 @@
   messageDataLocalID,
 } from '../types/message-types.js';
 import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
-import type { ResolvedNotifTexts } from '../types/notif-types.js';
+import type {
+  ResolvedNotifTexts,
+  NotificationTargetDevice,
+  TargetedNotificationWithPlatform,
+  SenderDeviceDescriptor,
+  EncryptedNotifUtilsAPI,
+} from '../types/notif-types.js';
 import type { RawThreadInfos } from '../types/thread-types.js';
 import type { UserInfos } from '../types/user-types.js';
 import { type GetENSNames } from '../utils/ens-helpers.js';
@@ -187,19 +202,16 @@
 }
 
 async function buildNotifText(
-  inputData: {
-    +rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
-    +userID: string,
-    +threadInfos: { +[id: string]: ThreadInfo },
-    +userInfos: UserInfos,
-  },
+  rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  userID: string,
+  threadInfos: { +[id: string]: ThreadInfo },
+  userInfos: UserInfos,
   getENSNames: ?GetENSNames,
   getFCNames: ?GetFCNames,
 ): Promise<?{
   +notifTexts: ResolvedNotifTexts,
   +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
 }> {
-  const { rawMessageInfos, userID, threadInfos, userInfos } = inputData;
   const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) =>
     createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos);
 
@@ -243,20 +255,277 @@
   return { notifTexts, newRawMessageInfos };
 }
 
-export type PerUserNotifBuildResult = {
-  +[userID: string]: $ReadOnlyArray<{
-    +notifTexts: ResolvedNotifTexts,
-    +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
-  }>,
+type BuildNotifsForUserDevicesInputData = {
+  +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
+  +rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  +userID: string,
+  +threadInfos: { +[id: string]: ThreadInfo },
+  +userInfos: UserInfos,
+  +getENSNames: ?GetENSNames,
+  +getFCNames: ?GetFCNames,
+  +devicesByPlatform: $ReadOnlyMap<
+    Platform,
+    $ReadOnlyMap<string, $ReadOnlyArray<NotificationTargetDevice>>,
+  >,
 };
 
-async function buildNotifsTexts(
-  pushInfo: PushInfo,
-  rawThreadInfos: RawThreadInfos,
-  userInfos: UserInfos,
-  getENSNames: ?GetENSNames,
-  getFCNames: ?GetFCNames,
-): Promise<PerUserNotifBuildResult> {
+async function buildNotifsForUserDevices(
+  inputData: BuildNotifsForUserDevicesInputData,
+): Promise<?$ReadOnlyArray<TargetedNotificationWithPlatform>> {
+  const {
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
+    rawMessageInfos,
+    userID,
+    threadInfos,
+    userInfos,
+    getENSNames,
+    getFCNames,
+    devicesByPlatform,
+  } = inputData;
+
+  const notifTextWithNewRawMessageInfos = await buildNotifText(
+    rawMessageInfos,
+    userID,
+    threadInfos,
+    userInfos,
+    getENSNames,
+    getFCNames,
+  );
+
+  if (!notifTextWithNewRawMessageInfos) {
+    return null;
+  }
+
+  const { notifTexts, newRawMessageInfos } = notifTextWithNewRawMessageInfos;
+  const [{ threadID }] = newRawMessageInfos;
+
+  const promises: Array<
+    Promise<$ReadOnlyArray<TargetedNotificationWithPlatform>>,
+  > = [];
+
+  const iosVersionToDevices = devicesByPlatform.get('ios');
+  if (iosVersionToDevices) {
+    for (const [versionKey, devices] of iosVersionToDevices) {
+      const { codeVersion, stateVersion } = stringToVersionKey(versionKey);
+      const platformDetails = {
+        platform: 'ios',
+        codeVersion,
+        stateVersion,
+      };
+      const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos(
+        newRawMessageInfos,
+        platformDetails,
+      );
+
+      promises.push(
+        (async () => {
+          return (
+            await createAPNsVisualNotification(
+              encryptedNotifUtilsAPI,
+              {
+                senderDeviceDescriptor,
+                notifTexts,
+                newRawMessageInfos: shimmedNewRawMessageInfos,
+                threadID,
+                collapseKey: undefined,
+                badgeOnly: false,
+                unreadCount: undefined,
+                platformDetails,
+                uniqueID: uuidv4(),
+              },
+              devices,
+            )
+          ).map(targetedNotification => ({
+            platform: 'ios',
+            targetedNotification,
+          }));
+        })(),
+      );
+    }
+  }
+
+  const androidVersionToDevices = devicesByPlatform.get('android');
+  if (androidVersionToDevices) {
+    for (const [versionKey, devices] of androidVersionToDevices) {
+      const { codeVersion, stateVersion } = stringToVersionKey(versionKey);
+      const platformDetails = {
+        platform: 'android',
+        codeVersion,
+        stateVersion,
+      };
+      const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos(
+        newRawMessageInfos,
+        platformDetails,
+      );
+
+      promises.push(
+        (async () => {
+          return (
+            await createAndroidVisualNotification(
+              encryptedNotifUtilsAPI,
+              {
+                senderDeviceDescriptor,
+                notifTexts,
+                newRawMessageInfos: shimmedNewRawMessageInfos,
+                threadID,
+                collapseKey: undefined,
+                badgeOnly: false,
+                unreadCount: undefined,
+                platformDetails,
+                notifID: uuidv4(),
+              },
+              devices,
+            )
+          ).map(targetedNotification => ({
+            platform: 'android',
+            targetedNotification,
+          }));
+        })(),
+      );
+    }
+  }
+
+  const macosVersionToDevices = devicesByPlatform.get('macos');
+  if (macosVersionToDevices) {
+    for (const [versionKey, devices] of macosVersionToDevices) {
+      const { codeVersion, stateVersion, majorDesktopVersion } =
+        stringToVersionKey(versionKey);
+      const platformDetails = {
+        platform: 'macos',
+        codeVersion,
+        stateVersion,
+        majorDesktopVersion,
+      };
+      const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos(
+        newRawMessageInfos,
+        platformDetails,
+      );
+
+      promises.push(
+        (async () => {
+          return (
+            await createAPNsVisualNotification(
+              encryptedNotifUtilsAPI,
+              {
+                senderDeviceDescriptor,
+                notifTexts,
+                newRawMessageInfos: shimmedNewRawMessageInfos,
+                threadID,
+                collapseKey: undefined,
+                badgeOnly: false,
+                unreadCount: undefined,
+                platformDetails,
+                uniqueID: uuidv4(),
+              },
+              devices,
+            )
+          ).map(targetedNotification => ({
+            platform: 'macos',
+            targetedNotification,
+          }));
+        })(),
+      );
+    }
+  }
+
+  const windowsVersionToDevices = devicesByPlatform.get('windows');
+  if (windowsVersionToDevices) {
+    for (const [versionKey, devices] of windowsVersionToDevices) {
+      const { codeVersion, stateVersion, majorDesktopVersion } =
+        stringToVersionKey(versionKey);
+      const platformDetails = {
+        platform: 'windows',
+        codeVersion,
+        stateVersion,
+        majorDesktopVersion,
+      };
+
+      promises.push(
+        (async () => {
+          return (
+            await createWNSNotification(
+              encryptedNotifUtilsAPI,
+              {
+                notifTexts,
+                threadID,
+                senderDeviceDescriptor,
+                platformDetails,
+              },
+              devices,
+            )
+          ).map(targetedNotification => ({
+            platform: 'windows',
+            targetedNotification,
+          }));
+        })(),
+      );
+    }
+  }
+
+  const webVersionToDevices = devicesByPlatform.get('web');
+  if (webVersionToDevices) {
+    for (const [versionKey, devices] of webVersionToDevices) {
+      const { codeVersion, stateVersion } = stringToVersionKey(versionKey);
+      const platformDetails = {
+        platform: 'web',
+        codeVersion,
+        stateVersion,
+      };
+
+      promises.push(
+        (async () => {
+          return (
+            await createWebNotification(
+              encryptedNotifUtilsAPI,
+              {
+                notifTexts,
+                threadID,
+                senderDeviceDescriptor,
+                platformDetails,
+                id: uuidv4(),
+              },
+              devices,
+            )
+          ).map(targetedNotification => ({
+            platform: 'web',
+            targetedNotification,
+          }));
+        })(),
+      );
+    }
+  }
+
+  return (await Promise.all(promises)).flat();
+}
+
+export type PerUserTargetedNotifications = {
+  +[userID: string]: $ReadOnlyArray<TargetedNotificationWithPlatform>,
+};
+
+type BuildNotifsFromPushInfoInputData = {
+  +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
+  +pushInfo: PushInfo,
+  +rawThreadInfos: RawThreadInfos,
+  +userInfos: UserInfos,
+  +getENSNames: ?GetENSNames,
+  +getFCNames: ?GetFCNames,
+};
+
+async function buildNotifsFromPushInfo(
+  inputData: BuildNotifsFromPushInfoInputData,
+): Promise<PerUserTargetedNotifications> {
+  const {
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
+    pushInfo,
+    rawThreadInfos,
+    userInfos,
+    getENSNames,
+    getFCNames,
+  } = inputData;
   const threadIDs = new Set<string>();
 
   for (const userID in pushInfo) {
@@ -273,13 +542,8 @@
     }
   }
 
-  const perUserNotifTextsPromises: {
-    [userID: string]: Promise<
-      Array<?{
-        +notifTexts: ResolvedNotifTexts,
-        +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
-      }>,
-    >,
+  const perUserBuildNotifsResultPromises: {
+    [userID: string]: Promise<$ReadOnlyArray<TargetedNotificationWithPlatform>>,
   } = {};
 
   for (const userID in pushInfo) {
@@ -293,41 +557,42 @@
         ),
       ]),
     );
-
-    const userNotifTextsPromises = [];
+    const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices);
+    const singleNotificationPromises = [];
 
     for (const rawMessageInfos of pushInfo[userID].messageInfos) {
-      userNotifTextsPromises.push(
-        buildNotifText(
-          {
-            // We always pass one element array here
-            // because coalescing is not supported for
-            // notifications generated on the client
-            rawMessageInfos: [rawMessageInfos],
-            threadInfos,
-            userID,
-            userInfos,
-          },
+      singleNotificationPromises.push(
+        // We always pass one element array here
+        // because coalescing is not supported for
+        // notifications generated on the client
+        buildNotifsForUserDevices({
+          encryptedNotifUtilsAPI,
+          senderDeviceDescriptor,
+          rawMessageInfos: [rawMessageInfos],
+          userID,
+          threadInfos,
+          userInfos,
           getENSNames,
           getFCNames,
-        ),
+          devicesByPlatform,
+        }),
       );
     }
 
-    perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises);
+    perUserBuildNotifsResultPromises[userID] = (async () => {
+      const singleNotificationResults = await Promise.all(
+        singleNotificationPromises,
+      );
+      return singleNotificationResults.filter(Boolean).flat();
+    })();
   }
 
-  const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises);
-  const filteredPerUserNotifTexts: { ...PerUserNotifBuildResult } = {};
-
-  for (const userID in perUserNotifTexts) {
-    filteredPerUserNotifTexts[userID] =
-      perUserNotifTexts[userID].filter(Boolean);
-  }
-  return filteredPerUserNotifTexts;
+  return promiseAll(perUserBuildNotifsResultPromises);
 }
 
 type PreparePushNotifsInputData = {
+  +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
   +messageInfos: { +[id: string]: RawMessageInfo },
   +rawThreadInfos: RawThreadInfos,
   +auxUserInfos: AuxUserInfos,
@@ -339,8 +604,10 @@
 
 async function preparePushNotifs(
   inputData: PreparePushNotifsInputData,
-): Promise<?PerUserNotifBuildResult> {
+): Promise<?PerUserTargetedNotifications> {
   const {
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
     messageDatas,
     messageInfos,
     auxUserInfos,
@@ -361,13 +628,15 @@
     return null;
   }
 
-  return await buildNotifsTexts(
-    pushInfos,
+  return await buildNotifsFromPushInfo({
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
+    pushInfo: pushInfos,
     rawThreadInfos,
     userInfos,
     getENSNames,
     getFCNames,
-  );
+  });
 }
 
 export { preparePushNotifs, generateNotifUserInfoPromise };
diff --git a/lib/push/web-notif-creators.js b/lib/push/web-notif-creators.js
--- a/lib/push/web-notif-creators.js
+++ b/lib/push/web-notif-creators.js
@@ -21,7 +21,7 @@
   +notifTexts: ResolvedNotifTexts,
   +threadID: string,
   +senderDeviceDescriptor: SenderDeviceDescriptor,
-  +unreadCount: number,
+  +unreadCount?: number,
   +platformDetails: PlatformDetails,
 };
 
@@ -31,7 +31,7 @@
     notifTexts: resolvedNotifTextsValidator,
     threadID: tID,
     senderDeviceDescriptor: senderDeviceDescriptorValidator,
-    unreadCount: t.Number,
+    unreadCount: t.maybe(t.Number),
     platformDetails: tPlatformDetails,
   });
 
diff --git a/lib/push/wns-notif-creators.js b/lib/push/wns-notif-creators.js
--- a/lib/push/wns-notif-creators.js
+++ b/lib/push/wns-notif-creators.js
@@ -22,7 +22,7 @@
   +notifTexts: ResolvedNotifTexts,
   +threadID: string,
   +senderDeviceDescriptor: SenderDeviceDescriptor,
-  +unreadCount: number,
+  +unreadCount?: number,
   +platformDetails: PlatformDetails,
 };
 
@@ -31,7 +31,7 @@
     notifTexts: resolvedNotifTextsValidator,
     threadID: tID,
     senderDeviceDescriptor: senderDeviceDescriptorValidator,
-    unreadCount: t.Number,
+    unreadCount: t.maybe(t.Number),
     platformDetails: tPlatformDetails,
   });
 
diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js
--- a/lib/types/notif-types.js
+++ b/lib/types/notif-types.js
@@ -3,6 +3,7 @@
 import type { EncryptResult } from '@commapp/olm';
 import t, { type TInterface, type TUnion } from 'tcomb';
 
+import type { Platform } from './device-types.js';
 import type { EntityText, ThreadEntity } from '../utils/entity-text.js';
 import { tShape } from '../utils/validation-utils.js';
 
@@ -42,7 +43,7 @@
   +body: string,
   +prefix?: string,
   +title: string,
-  +unreadCount: number,
+  +unreadCount?: number,
   +threadID: string,
   +encryptionFailed?: '1',
 };
@@ -68,7 +69,7 @@
   +body: string,
   +prefix?: string,
   +title: string,
-  +unreadCount: number,
+  +unreadCount?: number,
   +threadID: string,
   +encryptionFailed?: '1',
 };
@@ -361,6 +362,15 @@
   +blobHolder?: string,
 };
 
+export type TargetedNotificationWithPlatform = {
+  +platform: Platform,
+  +targetedNotification:
+    | TargetedAPNsNotification
+    | TargetedWNSNotification
+    | TargetedWebNotification
+    | TargetedAndroidNotification,
+};
+
 export type EncryptedNotifUtilsAPI = {
   +encryptSerializedNotifPayload: (
     cryptoID: string,