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
@@ -6,15 +6,6 @@
 import _cloneDeep from 'lodash/fp/cloneDeep.js';
 
 import type {
-  PlainTextWebNotification,
-  PlainTextWebNotificationPayload,
-  WebNotification,
-  PlainTextWNSNotification,
-  WNSNotification,
-  AndroidVisualNotification,
-  AndroidVisualNotificationPayload,
-  AndroidBadgeOnlyNotification,
-  AndroidNotificationRescind,
   NotificationTargetDevice,
   SenderDeviceDescriptor,
   EncryptedNotifUtilsAPI,
@@ -132,245 +123,6 @@
   }
 }
 
-async function encryptAndroidNotificationPayload<T>(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  unencryptedPayload: T,
-  payloadSizeValidator?: (
-    T | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
-  ) => boolean,
-): Promise<{
-  +resultPayload:
-    | T
-    | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
-  +payloadSizeExceeded: boolean,
-  +encryptionOrder?: number,
-}> {
-  try {
-    const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
-    if (!unencryptedSerializedPayload) {
-      return {
-        resultPayload: unencryptedPayload,
-        payloadSizeExceeded: payloadSizeValidator
-          ? payloadSizeValidator(unencryptedPayload)
-          : false,
-      };
-    }
-
-    let dbPersistCondition;
-    if (payloadSizeValidator) {
-      dbPersistCondition = (serializedPayload: string) =>
-        payloadSizeValidator({
-          encryptedPayload: serializedPayload,
-          ...senderDeviceDescriptor,
-        });
-    }
-
-    const {
-      encryptedData: serializedPayload,
-      sizeLimitViolated: dbPersistConditionViolated,
-      encryptionOrder,
-    } = await encryptedNotifUtilsAPI.encryptSerializedNotifPayload(
-      cookieID,
-      unencryptedSerializedPayload,
-      dbPersistCondition,
-    );
-
-    return {
-      resultPayload: {
-        encryptedPayload: serializedPayload.body,
-        ...senderDeviceDescriptor,
-      },
-      payloadSizeExceeded: !!dbPersistConditionViolated,
-      encryptionOrder,
-    };
-  } catch (e) {
-    console.log('Notification encryption failed: ' + e);
-    const resultPayload = {
-      encryptionFailed: '1',
-      ...unencryptedPayload,
-    };
-    return {
-      resultPayload,
-      payloadSizeExceeded: payloadSizeValidator
-        ? payloadSizeValidator(resultPayload)
-        : false,
-    };
-  }
-}
-
-async function encryptAndroidVisualNotification(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  cookieID: string,
-  notification: AndroidVisualNotification,
-  notificationSizeValidator?: AndroidVisualNotification => boolean,
-  blobHolder?: ?string,
-): Promise<{
-  +notification: AndroidVisualNotification,
-  +payloadSizeExceeded: boolean,
-  +encryptionOrder?: number,
-}> {
-  const { id, ...rest } = notification.data;
-
-  let unencryptedData = {};
-  if (id) {
-    unencryptedData = { id };
-  }
-
-  let unencryptedPayload = rest;
-  if (blobHolder) {
-    unencryptedPayload = { ...unencryptedPayload, blobHolder };
-  }
-
-  let payloadSizeValidator;
-  if (notificationSizeValidator) {
-    payloadSizeValidator = (
-      payload:
-        | AndroidVisualNotificationPayload
-        | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
-    ) => {
-      return notificationSizeValidator({
-        data: { ...unencryptedData, ...payload },
-      });
-    };
-  }
-  const { resultPayload, payloadSizeExceeded, encryptionOrder } =
-    await encryptAndroidNotificationPayload(
-      encryptedNotifUtilsAPI,
-      cookieID,
-      senderDeviceDescriptor,
-      unencryptedPayload,
-      payloadSizeValidator,
-    );
-  return {
-    notification: {
-      data: {
-        ...unencryptedData,
-        ...resultPayload,
-      },
-    },
-    payloadSizeExceeded,
-    encryptionOrder,
-  };
-}
-
-async function encryptAndroidSilentNotification(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification,
-): Promise<AndroidNotificationRescind | AndroidBadgeOnlyNotification> {
-  // We don't validate payload size for rescind
-  // since they are expected to be small and
-  // never exceed any FCM limit
-  const { ...unencryptedPayload } = notification.data;
-  const { resultPayload } = await encryptAndroidNotificationPayload(
-    encryptedNotifUtilsAPI,
-    cookieID,
-    senderDeviceDescriptor,
-    unencryptedPayload,
-  );
-  if (resultPayload.encryptedPayload) {
-    return {
-      data: { ...resultPayload },
-    };
-  }
-
-  if (resultPayload.rescind) {
-    return {
-      data: { ...resultPayload },
-    };
-  }
-
-  return {
-    data: {
-      ...resultPayload,
-    },
-  };
-}
-
-async function encryptBasicPayload<T>(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  basicPayload: T,
-): Promise<
-  | $ReadOnly<{
-      ...SenderDeviceDescriptor,
-      +encryptedPayload: string,
-      +encryptionOrder?: number,
-    }>
-  | { ...T, +encryptionFailed: '1' },
-> {
-  const unencryptedSerializedPayload = JSON.stringify(basicPayload);
-
-  if (!unencryptedSerializedPayload) {
-    return { ...basicPayload, encryptionFailed: '1' };
-  }
-
-  try {
-    const { encryptedData: serializedPayload, encryptionOrder } =
-      await encryptedNotifUtilsAPI.encryptSerializedNotifPayload(
-        cookieID,
-        unencryptedSerializedPayload,
-      );
-
-    return {
-      ...senderDeviceDescriptor,
-      encryptedPayload: serializedPayload.body,
-      encryptionOrder,
-    };
-  } catch (e) {
-    console.log('Notification encryption failed: ' + e);
-    return {
-      ...basicPayload,
-      encryptionFailed: '1',
-    };
-  }
-}
-
-async function encryptWebNotification(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  notification: PlainTextWebNotification,
-): Promise<{ +notification: WebNotification, +encryptionOrder?: number }> {
-  const { id, ...payloadSansId } = notification;
-  const { encryptionOrder, ...encryptionResult } =
-    await encryptBasicPayload<PlainTextWebNotificationPayload>(
-      encryptedNotifUtilsAPI,
-      cookieID,
-      senderDeviceDescriptor,
-      payloadSansId,
-    );
-
-  return {
-    notification: { id, ...encryptionResult },
-    encryptionOrder,
-  };
-}
-
-async function encryptWNSNotification(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  notification: PlainTextWNSNotification,
-): Promise<{ +notification: WNSNotification, +encryptionOrder?: number }> {
-  const { encryptionOrder, ...encryptionResult } =
-    await encryptBasicPayload<PlainTextWNSNotification>(
-      encryptedNotifUtilsAPI,
-      cookieID,
-      senderDeviceDescriptor,
-      notification,
-    );
-  return {
-    notification: { ...encryptionResult },
-    encryptionOrder,
-  };
-}
-
 function prepareEncryptedAPNsNotifications(
   encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
   senderDeviceDescriptor: SenderDeviceDescriptor,
@@ -433,118 +185,6 @@
   return Promise.all(notificationPromises);
 }
 
-function prepareEncryptedAndroidVisualNotifications(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: AndroidVisualNotification,
-  notificationSizeValidator?: (
-    notification: AndroidVisualNotification,
-  ) => boolean,
-): Promise<
-  $ReadOnlyArray<{
-    +cookieID: string,
-    +deviceToken: string,
-    +notification: AndroidVisualNotification,
-    +payloadSizeExceeded: boolean,
-    +encryptionOrder?: number,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ deviceToken, cookieID, blobHolder }) => {
-      const notif = await encryptAndroidVisualNotification(
-        encryptedNotifUtilsAPI,
-        senderDeviceDescriptor,
-        cookieID,
-        notification,
-        notificationSizeValidator,
-        blobHolder,
-      );
-      return { deviceToken, cookieID, ...notif };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
-function prepareEncryptedAndroidSilentNotifications(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification,
-): Promise<
-  $ReadOnlyArray<{
-    +cookieID: string,
-    +deviceToken: string,
-    +notification: AndroidNotificationRescind | AndroidBadgeOnlyNotification,
-    +encryptionOrder?: number,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ deviceToken, cookieID }) => {
-      const notif = await encryptAndroidSilentNotification(
-        encryptedNotifUtilsAPI,
-        cookieID,
-        senderDeviceDescriptor,
-        notification,
-      );
-      return { deviceToken, cookieID, notification: notif };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
-function prepareEncryptedWebNotifications(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: PlainTextWebNotification,
-): Promise<
-  $ReadOnlyArray<{
-    +deviceToken: string,
-    +notification: WebNotification,
-    +encryptionOrder?: number,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ deviceToken, cookieID }) => {
-      const notif = await encryptWebNotification(
-        encryptedNotifUtilsAPI,
-        cookieID,
-        senderDeviceDescriptor,
-        notification,
-      );
-      return { ...notif, deviceToken };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
-function prepareEncryptedWNSNotifications(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: PlainTextWNSNotification,
-): Promise<
-  $ReadOnlyArray<{
-    +deviceToken: string,
-    +notification: WNSNotification,
-    +encryptionOrder?: number,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ deviceToken, cookieID }) => {
-      const notif = await encryptWNSNotification(
-        encryptedNotifUtilsAPI,
-        cookieID,
-        senderDeviceDescriptor,
-        notification,
-      );
-      return { ...notif, deviceToken };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
 async function encryptBlobPayload(payload: string): Promise<{
   +encryptionKey: string,
   +encryptedPayload: Blob,
@@ -574,9 +214,5 @@
 export {
   prepareEncryptedAPNsNotifications,
   prepareEncryptedIOSNotificationRescind,
-  prepareEncryptedAndroidVisualNotifications,
-  prepareEncryptedAndroidSilentNotifications,
-  prepareEncryptedWebNotifications,
-  prepareEncryptedWNSNotifications,
   encryptBlobPayload,
 };
diff --git a/keyserver/src/push/encrypted-notif-utils-api.js b/keyserver/src/push/encrypted-notif-utils-api.js
--- a/keyserver/src/push/encrypted-notif-utils-api.js
+++ b/keyserver/src/push/encrypted-notif-utils-api.js
@@ -11,7 +11,10 @@
   encryptSerializedNotifPayload: async (
     cryptoID: string,
     unencryptedPayload: string,
-    encryptedPayloadSizeValidator?: (encryptedPayload: string) => boolean,
+    encryptedPayloadSizeValidator?: (
+      encryptedPayload: string,
+      type: '1' | '0',
+    ) => boolean,
   ) => {
     let dbPersistCondition;
     if (encryptedPayloadSizeValidator) {
@@ -19,7 +22,11 @@
         serializedPayload,
       }: {
         +[string]: EncryptResult,
-      }) => encryptedPayloadSizeValidator(serializedPayload.body);
+      }) =>
+        encryptedPayloadSizeValidator(
+          serializedPayload.body,
+          serializedPayload.type ? '1' : '0',
+        );
     }
 
     const {
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
@@ -5,6 +5,7 @@
 import type { FirebaseError } from 'firebase-admin';
 import invariant from 'invariant';
 
+import { prepareEncryptedAndroidSilentNotifications } from 'lib/push/crypto.js';
 import type { PlatformDetails } from 'lib/types/device-types.js';
 import type {
   NotificationTargetDevice,
@@ -17,10 +18,7 @@
 import { promiseAll } from 'lib/utils/promises.js';
 import { tID } from 'lib/utils/validation-utils.js';
 
-import {
-  prepareEncryptedAndroidSilentNotifications,
-  prepareEncryptedIOSNotificationRescind,
-} from './crypto.js';
+import { prepareEncryptedIOSNotificationRescind } from './crypto.js';
 import encryptedNotifUtilsAPI from './encrypted-notif-utils-api.js';
 import { getAPNsNotificationTopic } from './providers.js';
 import type { TargetedAPNsNotification } from './types.js';
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
@@ -12,6 +12,22 @@
 import t from 'tcomb';
 import uuidv4 from 'uuid/v4.js';
 
+import {
+  type AndroidNotifInputData,
+  androidNotifInputDataValidator,
+  createAndroidVisualNotification,
+} from 'lib/push/android-notif-creators.js';
+import { prepareEncryptedAndroidSilentNotifications } from 'lib/push/crypto.js';
+import {
+  type WebNotifInputData,
+  webNotifInputDataValidator,
+  createWebNotification,
+} from 'lib/push/web-notif-creators.js';
+import {
+  type WNSNotifInputData,
+  wnsNotifInputDataValidator,
+  createWNSNotification,
+} from 'lib/push/wns-notif-creators.js';
 import { oldValidUsernameRegex } from 'lib/shared/account-utils.js';
 import { isUserMentioned } from 'lib/shared/mention-utils.js';
 import {
@@ -35,7 +51,6 @@
 } from 'lib/types/message-types.js';
 import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
 import type {
-  AndroidVisualNotification,
   NotificationTargetDevice,
   TargetedAndroidNotification,
   TargetedWebNotification,
@@ -49,13 +64,7 @@
 import { values } from 'lib/utils/objects.js';
 import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js';
 
-import {
-  prepareEncryptedAndroidVisualNotifications,
-  prepareEncryptedAndroidSilentNotifications,
-  prepareEncryptedAPNsNotifications,
-  prepareEncryptedWebNotifications,
-  prepareEncryptedWNSNotifications,
-} from './crypto.js';
+import { prepareEncryptedAPNsNotifications } from './crypto.js';
 import encryptedNotifUtilsAPI from './encrypted-notif-utils-api.js';
 import { getAPNsNotificationTopic } from './providers.js';
 import { rescindPushNotifs } from './rescind.js';
@@ -63,12 +72,10 @@
 import {
   apnMaxNotificationPayloadByteSize,
   apnPush,
-  fcmMaxNotificationPayloadByteSize,
   fcmPush,
   getUnreadCounts,
   webPush,
   type WebPushError,
-  wnsMaxNotificationPayloadByteSize,
   wnsPush,
   type WNSPushError,
 } from './utils.js';
@@ -370,7 +377,7 @@
         (async () => {
           const targetedNotifications = await prepareAndroidVisualNotification(
             {
-              keyserverID,
+              senderDeviceDescriptor: { keyserverID },
               notifTexts,
               newRawMessageInfos: shimmedNewRawMessageInfos,
               threadID: threadInfo.id,
@@ -378,7 +385,7 @@
               badgeOnly,
               unreadCount,
               platformDetails,
-              dbID,
+              notifID: dbID,
             },
             devices,
           );
@@ -411,9 +418,10 @@
             {
               notifTexts,
               threadID: threadInfo.id,
-              keyserverID,
+              senderDeviceDescriptor: { keyserverID },
               unreadCount,
               platformDetails,
+              id: uuidv4(),
             },
             devices,
           );
@@ -491,7 +499,7 @@
           const targetedNotifications = await prepareWNSNotification(devices, {
             notifTexts,
             threadID: threadInfo.id,
-            keyserverID,
+            senderDeviceDescriptor: { keyserverID },
             unreadCount,
             platformDetails,
           });
@@ -1161,14 +1169,6 @@
   ];
 }
 
-type AndroidNotifInputData = {
-  ...CommonNativeNotifInputData,
-  +dbID: string,
-};
-const androidNotifInputDataValidator = tShape<AndroidNotifInputData>({
-  ...commonNativeNotifInputDataValidator.meta.props,
-  dbID: t.String,
-});
 async function prepareAndroidVisualNotification(
   inputData: AndroidNotifInputData,
   devices: $ReadOnlyArray<NotificationTargetDevice>,
@@ -1178,203 +1178,14 @@
     androidNotifInputDataValidator,
     inputData,
   );
-  const {
-    keyserverID,
-    notifTexts,
-    newRawMessageInfos,
-    threadID,
-    collapseKey,
-    badgeOnly,
-    unreadCount,
-    platformDetails,
-    dbID,
-  } = convertedData;
-
-  const canDecryptNonCollapsibleTextNotifs = hasMinCodeVersion(
-    platformDetails,
-    { native: 228 },
-  );
-  const isNonCollapsibleTextNotif =
-    newRawMessageInfos.every(
-      newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT,
-    ) && !collapseKey;
-
-  const canDecryptAllNotifTypes = hasMinCodeVersion(platformDetails, {
-    native: 267,
-  });
-
-  const shouldBeEncrypted =
-    canDecryptAllNotifTypes ||
-    (canDecryptNonCollapsibleTextNotifs && isNonCollapsibleTextNotif);
-
-  const { merged, ...rest } = notifTexts;
-  const notification = {
-    data: {
-      badge: unreadCount.toString(),
-      ...rest,
-      threadID,
-    },
-  };
-
-  let notifID;
-  if (collapseKey && canDecryptAllNotifTypes) {
-    notifID = dbID;
-    notification.data = {
-      ...notification.data,
-      collapseKey,
-    };
-  } else if (collapseKey) {
-    notifID = collapseKey;
-  } else {
-    notifID = dbID;
-  }
-
-  notification.data = {
-    ...notification.data,
-    id: notifID,
-    badgeOnly: badgeOnly ? '1' : '0',
-  };
-
-  const messageInfos = JSON.stringify(newRawMessageInfos);
-  const copyWithMessageInfos = {
-    ...notification,
-    data: { ...notification.data, messageInfos },
-  };
-
-  const priority = 'high';
-  if (!shouldBeEncrypted) {
-    const notificationToSend =
-      encryptedNotifUtilsAPI.getNotifByteSize(
-        JSON.stringify(copyWithMessageInfos),
-      ) <= fcmMaxNotificationPayloadByteSize
-        ? copyWithMessageInfos
-        : notification;
-
-    return devices.map(({ deviceToken }) => ({
-      priority,
-      notification: notificationToSend,
-      deviceToken,
-    }));
-  }
-
-  const notificationsSizeValidator = (notif: AndroidVisualNotification) => {
-    const serializedNotif = JSON.stringify(notif);
-    return (
-      !serializedNotif ||
-      encryptedNotifUtilsAPI.getNotifByteSize(serializedNotif) <=
-        fcmMaxNotificationPayloadByteSize
-    );
-  };
-
-  const notifsWithMessageInfos =
-    await prepareEncryptedAndroidVisualNotifications(
-      encryptedNotifUtilsAPI,
-      { keyserverID },
-      devices,
-      copyWithMessageInfos,
-      notificationsSizeValidator,
-    );
-
-  const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos
-    .filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
-    .map(({ cookieID, deviceToken }) => ({ cookieID, deviceToken }));
-
-  if (devicesWithExcessiveSizeNoHolders.length === 0) {
-    return notifsWithMessageInfos.map(
-      ({ notification: notif, deviceToken, encryptionOrder }) => ({
-        priority,
-        notification: notif,
-        deviceToken,
-        encryptionOrder,
-      }),
-    );
-  }
-
-  const canQueryBlobService = hasMinCodeVersion(platformDetails, {
-    native: 331,
-  });
-
-  let blobHash, blobHolders, encryptionKey, blobUploadError;
-  if (canQueryBlobService) {
-    ({ blobHash, blobHolders, encryptionKey, blobUploadError } =
-      await encryptedNotifUtilsAPI.uploadLargeNotifPayload(
-        JSON.stringify(copyWithMessageInfos.data),
-        devicesWithExcessiveSizeNoHolders.length,
-      ));
-  }
-
-  if (blobUploadError) {
-    console.warn(
-      `Failed to upload payload of notification: ${notifID} ` +
-        `due to error: ${blobUploadError}`,
-    );
-  }
-
-  let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders;
-  if (
-    blobHash &&
-    encryptionKey &&
-    blobHolders &&
-    blobHolders.length === devicesWithExcessiveSizeNoHolders.length
-  ) {
-    notification.data = {
-      ...notification.data,
-      blobHash,
-      encryptionKey,
-    };
-
-    devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({
-      ...devicesWithExcessiveSize[idx],
-      blobHolder: holder,
-    }));
-  }
 
-  const notifsWithoutMessageInfos =
-    await prepareEncryptedAndroidVisualNotifications(
-      encryptedNotifUtilsAPI,
-      { keyserverID },
-      devicesWithExcessiveSize,
-      notification,
-    );
-
-  const targetedNotifsWithMessageInfos = notifsWithMessageInfos
-    .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded)
-    .map(({ notification: notif, deviceToken, encryptionOrder }) => ({
-      priority,
-      notification: notif,
-      deviceToken,
-      encryptionOrder,
-    }));
-
-  const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map(
-    ({ notification: notif, deviceToken, encryptionOrder }) => ({
-      priority,
-      notification: notif,
-      deviceToken,
-      encryptionOrder,
-    }),
+  return createAndroidVisualNotification(
+    encryptedNotifUtilsAPI,
+    convertedData,
+    devices,
   );
-
-  return [
-    ...targetedNotifsWithMessageInfos,
-    ...targetedNotifsWithoutMessageInfos,
-  ];
 }
 
-type WebNotifInputData = {
-  +notifTexts: ResolvedNotifTexts,
-  +threadID: string,
-  +keyserverID: string,
-  +unreadCount: number,
-  +platformDetails: PlatformDetails,
-};
-const webNotifInputDataValidator = tShape<WebNotifInputData>({
-  notifTexts: resolvedNotifTextsValidator,
-  threadID: tID,
-  keyserverID: t.String,
-  unreadCount: t.Number,
-  platformDetails: tPlatformDetails,
-});
 async function prepareWebNotification(
   inputData: WebNotifInputData,
   devices: $ReadOnlyArray<NotificationTargetDevice>,
@@ -1384,46 +1195,10 @@
     webNotifInputDataValidator,
     inputData,
   );
-  const { notifTexts, threadID, unreadCount, keyserverID } = convertedData;
-  const id = uuidv4();
-  const { merged, ...rest } = notifTexts;
-  const notification = {
-    ...rest,
-    unreadCount,
-    id,
-    threadID,
-  };
-
-  const shouldBeEncrypted = hasMinCodeVersion(convertedData.platformDetails, {
-    web: 43,
-  });
-
-  if (!shouldBeEncrypted) {
-    return devices.map(({ deviceToken }) => ({ deviceToken, notification }));
-  }
 
-  return prepareEncryptedWebNotifications(
-    encryptedNotifUtilsAPI,
-    { keyserverID },
-    devices,
-    notification,
-  );
+  return createWebNotification(encryptedNotifUtilsAPI, convertedData, devices);
 }
 
-type WNSNotifInputData = {
-  +notifTexts: ResolvedNotifTexts,
-  +threadID: string,
-  +keyserverID: string,
-  +unreadCount: number,
-  +platformDetails: PlatformDetails,
-};
-const wnsNotifInputDataValidator = tShape<WNSNotifInputData>({
-  notifTexts: resolvedNotifTextsValidator,
-  threadID: tID,
-  keyserverID: t.String,
-  unreadCount: t.Number,
-  platformDetails: tPlatformDetails,
-});
 async function prepareWNSNotification(
   devices: $ReadOnlyArray<NotificationTargetDevice>,
   inputData: WNSNotifInputData,
@@ -1433,37 +1208,7 @@
     wnsNotifInputDataValidator,
     inputData,
   );
-  const { notifTexts, threadID, unreadCount, keyserverID } = convertedData;
-  const { merged, ...rest } = notifTexts;
-  const notification = {
-    ...rest,
-    unreadCount,
-    threadID,
-  };
-
-  if (
-    encryptedNotifUtilsAPI.getNotifByteSize(JSON.stringify(notification)) >
-    wnsMaxNotificationPayloadByteSize
-  ) {
-    console.warn('WNS notification exceeds size limit');
-  }
-
-  const shouldBeEncrypted = hasMinCodeVersion(inputData.platformDetails, {
-    majorDesktop: 10,
-  });
-
-  if (!shouldBeEncrypted) {
-    return devices.map(({ deviceToken }) => ({
-      deviceToken,
-      notification,
-    }));
-  }
-  return await prepareEncryptedWNSNotifications(
-    encryptedNotifUtilsAPI,
-    { keyserverID },
-    devices,
-    notification,
-  );
+  return createWNSNotification(encryptedNotifUtilsAPI, convertedData, devices);
 }
 
 type NotificationInfo =
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -8,6 +8,8 @@
 import uuid from 'uuid';
 import webpush from 'web-push';
 
+import { fcmMaxNotificationPayloadByteSize } from 'lib/push/android-notif-creators.js';
+import { wnsMaxNotificationPayloadByteSize } from 'lib/push/wns-notif-creators.js';
 import type { PlatformDetails } from 'lib/types/device-types.js';
 import type {
   TargetedAndroidNotification,
@@ -34,14 +36,13 @@
   'messaging/registration-token-not-registered',
   'messaging/invalid-registration-token',
 ]);
-const fcmMaxNotificationPayloadByteSize = 4000;
+
 const apnTokenInvalidationErrorCode = 410;
 const apnBadRequestErrorCode = 400;
 const apnBadTokenErrorString = 'BadDeviceToken';
 const apnMaxNotificationPayloadByteSize = 4096;
 const webInvalidTokenErrorCodes = [404, 410];
 const wnsInvalidTokenErrorCodes = [404, 410];
-const wnsMaxNotificationPayloadByteSize = 5000;
 
 export type APNPushResult =
   | { +success: true }
diff --git a/lib/push/android-notif-creators.js b/lib/push/android-notif-creators.js
new file mode 100644
--- /dev/null
+++ b/lib/push/android-notif-creators.js
@@ -0,0 +1,254 @@
+// @flow
+
+import t, { type TInterface } from 'tcomb';
+
+import { prepareEncryptedAndroidVisualNotifications } from './crypto.js';
+import { hasMinCodeVersion } from '../shared/version-utils.js';
+import type { PlatformDetails } from '../types/device-types.js';
+import { messageTypes } from '../types/message-types-enum.js';
+import {
+  type RawMessageInfo,
+  rawMessageInfoValidator,
+} from '../types/message-types.js';
+import {
+  type AndroidVisualNotification,
+  type NotificationTargetDevice,
+  type TargetedAndroidNotification,
+  type ResolvedNotifTexts,
+  resolvedNotifTextsValidator,
+  type SenderDeviceDescriptor,
+  senderDeviceDescriptorValidator,
+  type EncryptedNotifUtilsAPI,
+} from '../types/notif-types.js';
+import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js';
+
+export const fcmMaxNotificationPayloadByteSize = 4000;
+
+type CommonNativeNotifInputData = $ReadOnly<{
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
+  +notifTexts: ResolvedNotifTexts,
+  +newRawMessageInfos: RawMessageInfo[],
+  +threadID: string,
+  +collapseKey: ?string,
+  +badgeOnly: boolean,
+  +unreadCount?: number,
+  +platformDetails: PlatformDetails,
+}>;
+
+const commonNativeNotifInputDataValidator = tShape<CommonNativeNotifInputData>({
+  senderDeviceDescriptor: senderDeviceDescriptorValidator,
+  notifTexts: resolvedNotifTextsValidator,
+  newRawMessageInfos: t.list(rawMessageInfoValidator),
+  threadID: tID,
+  collapseKey: t.maybe(t.String),
+  badgeOnly: t.Boolean,
+  unreadCount: t.maybe(t.Number),
+  platformDetails: tPlatformDetails,
+});
+
+export type AndroidNotifInputData = {
+  ...CommonNativeNotifInputData,
+  +notifID: string,
+};
+
+export const androidNotifInputDataValidator: TInterface<AndroidNotifInputData> =
+  tShape<AndroidNotifInputData>({
+    ...commonNativeNotifInputDataValidator.meta.props,
+    notifID: t.String,
+  });
+
+async function createAndroidVisualNotification(
+  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  inputData: AndroidNotifInputData,
+  devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedAndroidNotification>> {
+  const {
+    senderDeviceDescriptor,
+    notifTexts,
+    newRawMessageInfos,
+    threadID,
+    collapseKey,
+    badgeOnly,
+    unreadCount,
+    platformDetails,
+    notifID,
+  } = inputData;
+
+  const canDecryptNonCollapsibleTextNotifs = hasMinCodeVersion(
+    platformDetails,
+    { native: 228 },
+  );
+  const isNonCollapsibleTextNotif =
+    newRawMessageInfos.every(
+      newRawMessageInfo => newRawMessageInfo.type === messageTypes.TEXT,
+    ) && !collapseKey;
+
+  const canDecryptAllNotifTypes = hasMinCodeVersion(platformDetails, {
+    native: 267,
+  });
+
+  const shouldBeEncrypted =
+    canDecryptAllNotifTypes ||
+    (canDecryptNonCollapsibleTextNotifs && isNonCollapsibleTextNotif);
+
+  const { merged, ...rest } = notifTexts;
+  const notification = {
+    data: {
+      ...rest,
+      threadID,
+    },
+  };
+
+  if (unreadCount) {
+    notification.data = {
+      ...notification.data,
+      badge: unreadCount.toString(),
+    };
+  }
+
+  let id;
+  if (collapseKey && canDecryptAllNotifTypes) {
+    id = notifID;
+    notification.data = {
+      ...notification.data,
+      collapseKey,
+    };
+  } else if (collapseKey) {
+    id = collapseKey;
+  } else {
+    id = notifID;
+  }
+
+  notification.data = {
+    ...notification.data,
+    id,
+    badgeOnly: badgeOnly ? '1' : '0',
+  };
+
+  const messageInfos = JSON.stringify(newRawMessageInfos);
+  const copyWithMessageInfos = {
+    ...notification,
+    data: { ...notification.data, messageInfos },
+  };
+
+  const priority = 'high';
+  if (!shouldBeEncrypted) {
+    const notificationToSend =
+      encryptedNotifUtilsAPI.getNotifByteSize(
+        JSON.stringify(copyWithMessageInfos),
+      ) <= fcmMaxNotificationPayloadByteSize
+        ? copyWithMessageInfos
+        : notification;
+
+    return devices.map(({ deviceToken }) => ({
+      priority,
+      notification: notificationToSend,
+      deviceToken,
+    }));
+  }
+
+  const notificationsSizeValidator = (notif: AndroidVisualNotification) => {
+    const serializedNotif = JSON.stringify(notif);
+    return (
+      !serializedNotif ||
+      encryptedNotifUtilsAPI.getNotifByteSize(serializedNotif) <=
+        fcmMaxNotificationPayloadByteSize
+    );
+  };
+
+  const notifsWithMessageInfos =
+    await prepareEncryptedAndroidVisualNotifications(
+      encryptedNotifUtilsAPI,
+      senderDeviceDescriptor,
+      devices,
+      copyWithMessageInfos,
+      notificationsSizeValidator,
+    );
+
+  const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos
+    .filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
+    .map(({ cookieID, deviceToken }) => ({ cookieID, deviceToken }));
+
+  if (devicesWithExcessiveSizeNoHolders.length === 0) {
+    return notifsWithMessageInfos.map(
+      ({ notification: notif, deviceToken, encryptionOrder }) => ({
+        priority,
+        notification: notif,
+        deviceToken,
+        encryptionOrder,
+      }),
+    );
+  }
+
+  const canQueryBlobService = hasMinCodeVersion(platformDetails, {
+    native: 331,
+  });
+
+  let blobHash, blobHolders, encryptionKey, blobUploadError;
+  if (canQueryBlobService) {
+    ({ blobHash, blobHolders, encryptionKey, blobUploadError } =
+      await encryptedNotifUtilsAPI.uploadLargeNotifPayload(
+        JSON.stringify(copyWithMessageInfos.data),
+        devicesWithExcessiveSizeNoHolders.length,
+      ));
+  }
+
+  if (blobUploadError) {
+    console.warn(
+      `Failed to upload payload of notification: ${notifID} ` +
+        `due to error: ${blobUploadError}`,
+    );
+  }
+
+  let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders;
+  if (
+    blobHash &&
+    encryptionKey &&
+    blobHolders &&
+    blobHolders.length === devicesWithExcessiveSizeNoHolders.length
+  ) {
+    notification.data = {
+      ...notification.data,
+      blobHash,
+      encryptionKey,
+    };
+
+    devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({
+      ...devicesWithExcessiveSize[idx],
+      blobHolder: holder,
+    }));
+  }
+
+  const notifsWithoutMessageInfos =
+    await prepareEncryptedAndroidVisualNotifications(
+      encryptedNotifUtilsAPI,
+      senderDeviceDescriptor,
+      devicesWithExcessiveSize,
+      notification,
+    );
+
+  const targetedNotifsWithMessageInfos = notifsWithMessageInfos
+    .filter(({ payloadSizeExceeded }) => !payloadSizeExceeded)
+    .map(({ notification: notif, deviceToken, encryptionOrder }) => ({
+      priority,
+      notification: notif,
+      deviceToken,
+      encryptionOrder,
+    }));
+
+  const targetedNotifsWithoutMessageInfos = notifsWithoutMessageInfos.map(
+    ({ notification: notif, deviceToken, encryptionOrder }) => ({
+      priority,
+      notification: notif,
+      deviceToken,
+      encryptionOrder,
+    }),
+  );
+
+  return [
+    ...targetedNotifsWithMessageInfos,
+    ...targetedNotifsWithoutMessageInfos,
+  ];
+}
+
+export { createAndroidVisualNotification };
diff --git a/keyserver/src/push/crypto.js b/lib/push/crypto.js
copy from keyserver/src/push/crypto.js
copy to lib/push/crypto.js
--- a/keyserver/src/push/crypto.js
+++ b/lib/push/crypto.js
@@ -1,10 +1,5 @@
 // @flow
 
-import apn from '@parse/node-apn';
-import crypto from 'crypto';
-import invariant from 'invariant';
-import _cloneDeep from 'lodash/fp/cloneDeep.js';
-
 import type {
   PlainTextWebNotification,
   PlainTextWebNotificationPayload,
@@ -18,119 +13,7 @@
   NotificationTargetDevice,
   SenderDeviceDescriptor,
   EncryptedNotifUtilsAPI,
-} from 'lib/types/notif-types.js';
-import { toBase64URL } from 'lib/utils/base64.js';
-
-import { encrypt, generateKey } from '../utils/aes-crypto-utils.js';
-import { getOlmUtility } from '../utils/olm-utils.js';
-
-async function encryptAPNsNotification(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  cookieID: string,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  notification: apn.Notification,
-  codeVersion?: ?number,
-  notificationSizeValidator?: apn.Notification => boolean,
-  blobHolder?: ?string,
-): Promise<{
-  +notification: apn.Notification,
-  +payloadSizeExceeded: boolean,
-  +encryptedPayloadHash?: string,
-  +encryptionOrder?: number,
-}> {
-  invariant(
-    !notification.collapseId,
-    `Collapse ID can't be directly stored in apn.Notification object due ` +
-      `to security reasons. Please put it in payload property`,
-  );
-
-  const encryptedNotification = new apn.Notification();
-
-  encryptedNotification.id = notification.id;
-  encryptedNotification.payload.id = notification.id;
-
-  if (blobHolder) {
-    encryptedNotification.payload.blobHolder = blobHolder;
-  }
-
-  encryptedNotification.payload.keyserverID = notification.payload.keyserverID;
-  encryptedNotification.topic = notification.topic;
-  encryptedNotification.sound = notification.aps.sound;
-  encryptedNotification.pushType = 'alert';
-  encryptedNotification.mutableContent = true;
-
-  const { id, ...payloadSansUnencryptedData } = notification.payload;
-  const unencryptedPayload = {
-    ...payloadSansUnencryptedData,
-    badge: notification.aps.badge.toString(),
-    merged: notification.body,
-  };
-
-  try {
-    const unencryptedSerializedPayload = JSON.stringify(unencryptedPayload);
-
-    let dbPersistCondition;
-    if (notificationSizeValidator) {
-      dbPersistCondition = (serializedPayload: string) => {
-        const notifCopy = _cloneDeep(encryptedNotification);
-        notifCopy.payload.encryptedPayload = serializedPayload;
-        return notificationSizeValidator(notifCopy);
-      };
-    }
-    const {
-      encryptedData: serializedPayload,
-      sizeLimitViolated: dbPersistConditionViolated,
-      encryptionOrder,
-    } = await encryptedNotifUtilsAPI.encryptSerializedNotifPayload(
-      cookieID,
-      unencryptedSerializedPayload,
-      dbPersistCondition,
-    );
-
-    encryptedNotification.payload.encryptedPayload = serializedPayload.body;
-    encryptedNotification.payload = {
-      ...encryptedNotification.payload,
-      ...senderDeviceDescriptor,
-    };
-
-    if (codeVersion && codeVersion >= 254 && codeVersion % 2 === 0) {
-      encryptedNotification.aps = {
-        alert: { body: 'ENCRYPTED' },
-        ...encryptedNotification.aps,
-      };
-    }
-
-    const encryptedPayloadHash = getOlmUtility().sha256(serializedPayload.body);
-    return {
-      notification: encryptedNotification,
-      payloadSizeExceeded: !!dbPersistConditionViolated,
-      encryptedPayloadHash,
-      encryptionOrder,
-    };
-  } catch (e) {
-    console.log('Notification encryption failed: ' + e);
-
-    encryptedNotification.body = notification.body;
-    encryptedNotification.threadId = notification.payload.threadID;
-    invariant(
-      typeof notification.aps.badge === 'number',
-      'Unencrypted notification must have badge as a number',
-    );
-    encryptedNotification.badge = notification.aps.badge;
-
-    encryptedNotification.payload = {
-      ...encryptedNotification.payload,
-      ...notification.payload,
-      encryptionFailed: 1,
-    };
-    return {
-      notification: encryptedNotification,
-      payloadSizeExceeded: notificationSizeValidator
-        ? notificationSizeValidator(_cloneDeep(encryptedNotification))
-        : false,
-    };
-  }
-}
+} from '../types/notif-types.js';
 
 async function encryptAndroidNotificationPayload<T>(
   encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
@@ -138,12 +21,21 @@
   senderDeviceDescriptor: SenderDeviceDescriptor,
   unencryptedPayload: T,
   payloadSizeValidator?: (
-    T | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
+    | T
+    | $ReadOnly<{
+        ...SenderDeviceDescriptor,
+        +encryptedPayload: string,
+        +type: '1' | '0',
+      }>,
   ) => boolean,
 ): Promise<{
   +resultPayload:
     | T
-    | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
+    | $ReadOnly<{
+        ...SenderDeviceDescriptor,
+        +encryptedPayload: string,
+        +type: '1' | '0',
+      }>,
   +payloadSizeExceeded: boolean,
   +encryptionOrder?: number,
 }> {
@@ -160,9 +52,10 @@
 
     let dbPersistCondition;
     if (payloadSizeValidator) {
-      dbPersistCondition = (serializedPayload: string) =>
+      dbPersistCondition = (serializedPayload: string, type: '1' | '0') =>
         payloadSizeValidator({
           encryptedPayload: serializedPayload,
+          type,
           ...senderDeviceDescriptor,
         });
     }
@@ -179,8 +72,9 @@
 
     return {
       resultPayload: {
-        encryptedPayload: serializedPayload.body,
         ...senderDeviceDescriptor,
+        encryptedPayload: serializedPayload.body,
+        type: serializedPayload.type ? '1' : '0',
       },
       payloadSizeExceeded: !!dbPersistConditionViolated,
       encryptionOrder,
@@ -229,7 +123,11 @@
     payloadSizeValidator = (
       payload:
         | AndroidVisualNotificationPayload
-        | $ReadOnly<{ ...SenderDeviceDescriptor, +encryptedPayload: string }>,
+        | $ReadOnly<{
+            ...SenderDeviceDescriptor,
+            +encryptedPayload: string,
+            +type: '0' | '1',
+          }>,
     ) => {
       return notificationSizeValidator({
         data: { ...unencryptedData, ...payload },
@@ -300,6 +198,7 @@
   | $ReadOnly<{
       ...SenderDeviceDescriptor,
       +encryptedPayload: string,
+      +type: '1' | '0',
       +encryptionOrder?: number,
     }>
   | { ...T, +encryptionFailed: '1' },
@@ -320,6 +219,7 @@
     return {
       ...senderDeviceDescriptor,
       encryptedPayload: serializedPayload.body,
+      type: serializedPayload.type ? '1' : '0',
       encryptionOrder,
     };
   } catch (e) {
@@ -371,68 +271,6 @@
   };
 }
 
-function prepareEncryptedAPNsNotifications(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: apn.Notification,
-  codeVersion?: ?number,
-  notificationSizeValidator?: apn.Notification => boolean,
-): Promise<
-  $ReadOnlyArray<{
-    +cookieID: string,
-    +deviceToken: string,
-    +notification: apn.Notification,
-    +payloadSizeExceeded: boolean,
-    +encryptedPayloadHash?: string,
-    +encryptionOrder?: number,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ cookieID, deviceToken, blobHolder }) => {
-      const notif = await encryptAPNsNotification(
-        encryptedNotifUtilsAPI,
-        cookieID,
-        senderDeviceDescriptor,
-        notification,
-        codeVersion,
-        notificationSizeValidator,
-        blobHolder,
-      );
-      return { cookieID, deviceToken, ...notif };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
-function prepareEncryptedIOSNotificationRescind(
-  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
-  senderDeviceDescriptor: SenderDeviceDescriptor,
-  devices: $ReadOnlyArray<NotificationTargetDevice>,
-  notification: apn.Notification,
-  codeVersion?: ?number,
-): Promise<
-  $ReadOnlyArray<{
-    +cookieID: string,
-    +deviceToken: string,
-    +notification: apn.Notification,
-  }>,
-> {
-  const notificationPromises = devices.map(
-    async ({ deviceToken, cookieID }) => {
-      const { notification: notif } = await encryptAPNsNotification(
-        encryptedNotifUtilsAPI,
-        cookieID,
-        senderDeviceDescriptor,
-        notification,
-        codeVersion,
-      );
-      return { deviceToken, cookieID, notification: notif };
-    },
-  );
-  return Promise.all(notificationPromises);
-}
-
 function prepareEncryptedAndroidVisualNotifications(
   encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
   senderDeviceDescriptor: SenderDeviceDescriptor,
@@ -545,38 +383,9 @@
   return Promise.all(notificationPromises);
 }
 
-async function encryptBlobPayload(payload: string): Promise<{
-  +encryptionKey: string,
-  +encryptedPayload: Blob,
-  +encryptedPayloadHash: string,
-}> {
-  const encryptionKey = await generateKey();
-  const encryptedPayload = await encrypt(
-    encryptionKey,
-    new TextEncoder().encode(payload),
-  );
-  const encryptedPayloadBuffer = Buffer.from(encryptedPayload);
-  const blobHashBase64 = await crypto
-    .createHash('sha256')
-    .update(encryptedPayloadBuffer)
-    .digest('base64');
-  const blobHash = toBase64URL(blobHashBase64);
-
-  const payloadBlob = new Blob([encryptedPayloadBuffer]);
-  const encryptionKeyString = Buffer.from(encryptionKey).toString('base64');
-  return {
-    encryptionKey: encryptionKeyString,
-    encryptedPayload: payloadBlob,
-    encryptedPayloadHash: blobHash,
-  };
-}
-
 export {
-  prepareEncryptedAPNsNotifications,
-  prepareEncryptedIOSNotificationRescind,
   prepareEncryptedAndroidVisualNotifications,
   prepareEncryptedAndroidSilentNotifications,
   prepareEncryptedWebNotifications,
   prepareEncryptedWNSNotifications,
-  encryptBlobPayload,
 };
diff --git a/lib/push/web-notif-creators.js b/lib/push/web-notif-creators.js
new file mode 100644
--- /dev/null
+++ b/lib/push/web-notif-creators.js
@@ -0,0 +1,70 @@
+// @flow
+
+import t, { type TInterface } from 'tcomb';
+
+import { prepareEncryptedWebNotifications } from './crypto.js';
+import { hasMinCodeVersion } from '../shared/version-utils.js';
+import type { PlatformDetails } from '../types/device-types.js';
+import {
+  type NotificationTargetDevice,
+  type TargetedWebNotification,
+  type ResolvedNotifTexts,
+  resolvedNotifTextsValidator,
+  type SenderDeviceDescriptor,
+  senderDeviceDescriptorValidator,
+  type EncryptedNotifUtilsAPI,
+} from '../types/notif-types.js';
+import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js';
+
+export type WebNotifInputData = {
+  +id: string,
+  +notifTexts: ResolvedNotifTexts,
+  +threadID: string,
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
+  +unreadCount: number,
+  +platformDetails: PlatformDetails,
+};
+
+export const webNotifInputDataValidator: TInterface<WebNotifInputData> =
+  tShape<WebNotifInputData>({
+    id: t.String,
+    notifTexts: resolvedNotifTextsValidator,
+    threadID: tID,
+    senderDeviceDescriptor: senderDeviceDescriptorValidator,
+    unreadCount: t.Number,
+    platformDetails: tPlatformDetails,
+  });
+
+async function createWebNotification(
+  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  inputData: WebNotifInputData,
+  devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedWebNotification>> {
+  const { id, notifTexts, threadID, unreadCount, senderDeviceDescriptor } =
+    inputData;
+
+  const { merged, ...rest } = notifTexts;
+  const notification = {
+    ...rest,
+    unreadCount,
+    id,
+    threadID,
+  };
+
+  const shouldBeEncrypted = hasMinCodeVersion(inputData.platformDetails, {
+    web: 43,
+  });
+
+  if (!shouldBeEncrypted) {
+    return devices.map(({ deviceToken }) => ({ deviceToken, notification }));
+  }
+
+  return prepareEncryptedWebNotifications(
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
+    devices,
+    notification,
+  );
+}
+
+export { createWebNotification };
diff --git a/lib/push/wns-notif-creators.js b/lib/push/wns-notif-creators.js
new file mode 100644
--- /dev/null
+++ b/lib/push/wns-notif-creators.js
@@ -0,0 +1,77 @@
+// @flow
+
+import t, { type TInterface } from 'tcomb';
+
+import { prepareEncryptedWNSNotifications } from './crypto.js';
+import { hasMinCodeVersion } from '../shared/version-utils.js';
+import type { PlatformDetails } from '../types/device-types.js';
+import {
+  type NotificationTargetDevice,
+  type TargetedWNSNotification,
+  type ResolvedNotifTexts,
+  resolvedNotifTextsValidator,
+  type SenderDeviceDescriptor,
+  senderDeviceDescriptorValidator,
+  type EncryptedNotifUtilsAPI,
+} from '../types/notif-types.js';
+import { tID, tPlatformDetails, tShape } from '../utils/validation-utils.js';
+
+export const wnsMaxNotificationPayloadByteSize = 5000;
+
+export type WNSNotifInputData = {
+  +notifTexts: ResolvedNotifTexts,
+  +threadID: string,
+  +senderDeviceDescriptor: SenderDeviceDescriptor,
+  +unreadCount: number,
+  +platformDetails: PlatformDetails,
+};
+
+export const wnsNotifInputDataValidator: TInterface<WNSNotifInputData> =
+  tShape<WNSNotifInputData>({
+    notifTexts: resolvedNotifTextsValidator,
+    threadID: tID,
+    senderDeviceDescriptor: senderDeviceDescriptorValidator,
+    unreadCount: t.Number,
+    platformDetails: tPlatformDetails,
+  });
+
+async function createWNSNotification(
+  encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI,
+  inputData: WNSNotifInputData,
+  devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedWNSNotification>> {
+  const { notifTexts, threadID, unreadCount, senderDeviceDescriptor } =
+    inputData;
+  const { merged, ...rest } = notifTexts;
+  const notification = {
+    ...rest,
+    unreadCount,
+    threadID,
+  };
+
+  if (
+    encryptedNotifUtilsAPI.getNotifByteSize(JSON.stringify(notification)) >
+    wnsMaxNotificationPayloadByteSize
+  ) {
+    console.warn('WNS notification exceeds size limit');
+  }
+
+  const shouldBeEncrypted = hasMinCodeVersion(inputData.platformDetails, {
+    majorDesktop: 10,
+  });
+
+  if (!shouldBeEncrypted) {
+    return devices.map(({ deviceToken }) => ({
+      deviceToken,
+      notification,
+    }));
+  }
+  return await prepareEncryptedWNSNotifications(
+    encryptedNotifUtilsAPI,
+    senderDeviceDescriptor,
+    devices,
+    notification,
+  );
+}
+
+export { createWNSNotification };
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
@@ -1,7 +1,7 @@
 // @flow
 
 import type { EncryptResult } from '@commapp/olm';
-import t, { type TInterface } from 'tcomb';
+import t, { type TInterface, type TUnion } from 'tcomb';
 
 import type { EntityText, ThreadEntity } from '../utils/entity-text.js';
 import { tShape } from '../utils/validation-utils.js';
@@ -31,6 +31,12 @@
   | { +keyserverID: string }
   | { +senderDeviceID: string };
 
+export const senderDeviceDescriptorValidator: TUnion<SenderDeviceDescriptor> =
+  t.union([
+    tShape({ keyserverID: t.String }),
+    tShape({ senderDeviceID: t.String }),
+  ]);
+
 export type PlainTextWebNotificationPayload = {
   +body: string,
   +prefix?: string,
@@ -49,7 +55,7 @@
   ...SenderDeviceDescriptor,
   +id: string,
   +encryptedPayload: string,
-  +type?: '0' | '1',
+  +type: '0' | '1',
 }>;
 
 export type WebNotification =
@@ -68,7 +74,7 @@
 export type EncryptedWNSNotification = $ReadOnly<{
   ...SenderDeviceDescriptor,
   +encryptedPayload: string,
-  +type?: '0' | '1',
+  +type: '0' | '1',
 }>;
 
 export type WNSNotification =
@@ -76,7 +82,7 @@
   | EncryptedWNSNotification;
 
 export type AndroidVisualNotificationPayloadBase = $ReadOnly<{
-  +badge: string,
+  +badge?: string,
   +body: string,
   +title: string,
   +prefix?: string,
@@ -104,13 +110,13 @@
 type EncryptedThinThreadPayload = {
   +keyserverID: string,
   +encryptedPayload: string,
-  +type?: '0' | '1',
+  +type: '0' | '1',
 };
 
 type EncryptedThickThreadPayload = {
   +senderDeviceID: string,
   +encryptedPayload: string,
-  +type?: '0' | '1',
+  +type: '0' | '1',
 };
 
 export type AndroidVisualNotification = {
@@ -191,7 +197,10 @@
   +encryptSerializedNotifPayload: (
     cryptoID: string,
     unencryptedPayload: string,
-    encryptedPayloadSizeValidator?: (encryptedPayload: string) => boolean,
+    encryptedPayloadSizeValidator?: (
+      encryptedPayload: string,
+      type: '1' | '0',
+    ) => boolean,
   ) => Promise<{
     +encryptedData: EncryptResult,
     +sizeLimitViolated?: boolean,