diff --git a/desktop/flow-typed/npm/electron_v22.0.0.js b/desktop/flow-typed/npm/electron_v22.0.0.js
--- a/desktop/flow-typed/npm/electron_v22.0.0.js
+++ b/desktop/flow-typed/npm/electron_v22.0.0.js
@@ -336,7 +336,7 @@
   declare type PushNotificationsEvents = {
     'received-apns-notification': (
       event: Event,
-      userInfo: { +[string]: mixed },
+      userInfo: { +[string]: mixed, +encryptedPayload?: string },
     ) => void,
   };
 
diff --git a/desktop/src/main.js b/desktop/src/main.js
--- a/desktop/src/main.js
+++ b/desktop/src/main.js
@@ -20,6 +20,7 @@
 import {
   listenForNotifications,
   registerForNotifications,
+  showNewNotification,
 } from './push-notifications.js';
 
 const isDev = process.env.ENV === 'dev';
@@ -303,13 +304,24 @@
       }
     };
 
+    const handleEncryptedNotification = (encryptedPayload: string) => {
+      if (mainWindow) {
+        mainWindow.webContents.send('on-encrypted-notification', {
+          encryptedPayload,
+        });
+      }
+    };
+
     if (app.isPackaged) {
       try {
         initAutoUpdate();
       } catch (error) {
         console.error(error);
       }
-      listenForNotifications(handleNotificationClick);
+      listenForNotifications(
+        handleNotificationClick,
+        handleEncryptedNotification,
+      );
       ipcMain.on('fetch-device-token', sendDeviceTokenToWebApp);
     }
 
@@ -321,6 +333,12 @@
     ipcMain.on('get-version', event => {
       event.returnValue = app.getVersion().toString();
     });
+    ipcMain.on(
+      'show-decrypted-notification',
+      (event, decryptedNotification) => {
+        showNewNotification(decryptedNotification, handleNotificationClick);
+      },
+    );
 
     show();
 
diff --git a/desktop/src/preload.js b/desktop/src/preload.js
--- a/desktop/src/preload.js
+++ b/desktop/src/preload.js
@@ -41,6 +41,19 @@
       ipcRenderer.removeListener('on-notification-clicked', withEvent);
   },
   fetchDeviceToken: () => ipcRenderer.send('fetch-device-token'),
+  onEncryptedNotification: callback => {
+    const withEvent = (
+      event: IpcRendererEvent,
+      ...args: $ReadOnlyArray<any>
+    ) => {
+      callback(...args);
+    };
+    ipcRenderer.on('on-encrypted-notification', withEvent);
+    return () =>
+      ipcRenderer.removeListener('on-encrypted-notification', withEvent);
+  },
+  showDecryptedNotification: decryptedPayload =>
+    ipcRenderer.send('show-decrypted-notification', decryptedPayload),
 };
 
 contextBridge.exposeInMainWorld('electronContextBridge', bridge);
diff --git a/desktop/src/push-notifications.js b/desktop/src/push-notifications.js
--- a/desktop/src/push-notifications.js
+++ b/desktop/src/push-notifications.js
@@ -127,10 +127,17 @@
   notif.show();
 }
 
-function listenForNotifications(handleClick: (threadID?: string) => void) {
+function listenForNotifications(
+  handleClick: (threadID?: string) => void,
+  handleEncryptedNotification: (encryptedPayload: string) => void,
+) {
   if (process.platform === 'darwin') {
     pushNotifications.on('received-apns-notification', (event, userInfo) => {
-      showNewNotification(userInfo, handleClick);
+      if (userInfo.encryptedPayload) {
+        handleEncryptedNotification(userInfo.encryptedPayload);
+      } else {
+        showNewNotification(userInfo, handleClick);
+      }
     });
   } else if (process.platform === 'win32') {
     windowsPushNotifEventEmitter.on('received-wns-notification', payload => {
@@ -138,4 +145,8 @@
     });
   }
 }
-export { listenForNotifications, registerForNotifications };
+export {
+  listenForNotifications,
+  registerForNotifications,
+  showNewNotification,
+};
diff --git a/lib/types/electron-types.js b/lib/types/electron-types.js
--- a/lib/types/electron-types.js
+++ b/lib/types/electron-types.js
@@ -11,6 +11,10 @@
 
 type OnNotificationClickedListener = (data: { threadID: string }) => void;
 
+type OnEncryptedNotificationListener = (data: {
+  encryptedPayload: string,
+}) => mixed;
+
 export type ElectronBridge = {
   // Returns a callback that you can call to remove the listener
   +onNavigate: OnNavigateListener => () => void,
@@ -25,4 +29,6 @@
   +onDeviceTokenRegistered?: OnDeviceTokenRegisteredListener => () => void,
   +onNotificationClicked?: OnNotificationClickedListener => () => void,
   +fetchDeviceToken: () => void,
+  +onEncryptedNotification?: OnEncryptedNotificationListener => () => void,
+  +showDecryptedNotification: (decryptedPayload: { +[string]: mixed }) => void,
 };
diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js
--- a/web/push-notif/push-notifs-handler.js
+++ b/web/push-notif/push-notifs-handler.js
@@ -17,6 +17,7 @@
 import { useDispatch } from 'lib/utils/redux-utils.js';
 import { ashoatKeyserverID } from 'lib/utils/validation-utils.js';
 
+import { decryptDesktopNotification } from './notif-crypto-utils.js';
 import {
   WORKERS_MODULES_DIR_PATH,
   DEFAULT_OLM_FILENAME,
@@ -33,6 +34,7 @@
 function useCreateDesktopPushSubscription() {
   const dispatchActionPromise = useDispatchActionPromise();
   const callSetDeviceToken = useSetDeviceTokenFanout();
+  const staffCanSee = useStaffCanSee();
 
   React.useEffect(
     () =>
@@ -49,6 +51,20 @@
     electron?.fetchDeviceToken();
   }, []);
 
+  React.useEffect(
+    () =>
+      electron?.onEncryptedNotification?.(
+        async ({ encryptedPayload }: { encryptedPayload: string }) => {
+          const decryptedPayload = await decryptDesktopNotification(
+            encryptedPayload,
+            staffCanSee,
+          );
+          electron?.showDecryptedNotification(decryptedPayload);
+        },
+      ),
+    [staffCanSee],
+  );
+
   const dispatch = useDispatch();
 
   React.useEffect(