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,11 @@
 import invariant from 'invariant';
 import _cloneDeep from 'lodash/fp/cloneDeep.js';
 
+import type {
+  PlainTextWebNotification,
+  WebNotification,
+} from 'lib/types/notif-types.js';
+
 import type {
   AndroidNotification,
   AndroidNotificationPayload,
@@ -213,6 +218,31 @@
   };
 }
 
+async function encryptWebNotification(
+  cookieID: string,
+  notification: PlainTextWebNotification,
+): Promise<WebNotification> {
+  const { id, ...payloadSansId } = notification;
+  const unencryptedSerializedPayload = JSON.stringify(payloadSansId);
+
+  try {
+    const {
+      encryptedMessages: { serializedPayload },
+    } = await encryptAndUpdateOlmSession(cookieID, 'notifications', {
+      serializedPayload: unencryptedSerializedPayload,
+    });
+
+    return { id, encryptedPayload: serializedPayload.body };
+  } catch (e) {
+    console.log('Notification encryption failed: ' + e);
+    return {
+      id,
+      encryptionFailed: '1',
+      ...payloadSansId,
+    };
+  }
+}
+
 function prepareEncryptedIOSNotifications(
   devices: $ReadOnlyArray<NotificationTargetDevice>,
   notification: apn.Notification,
@@ -312,9 +342,28 @@
   return Promise.all(notificationPromises);
 }
 
+function prepareEncryptedWebNotifications(
+  devices: $ReadOnlyArray<NotificationTargetDevice>,
+  notification: PlainTextWebNotification,
+): Promise<
+  $ReadOnlyArray<{
+    +deviceToken: string,
+    +notification: WebNotification,
+  }>,
+> {
+  const notificationPromises = devices.map(
+    async ({ deviceToken, cookieID }) => {
+      const notif = await encryptWebNotification(cookieID, notification);
+      return { notification: notif, deviceToken };
+    },
+  );
+  return Promise.all(notificationPromises);
+}
+
 export {
   prepareEncryptedIOSNotifications,
   prepareEncryptedIOSNotificationRescind,
   prepareEncryptedAndroidNotifications,
   prepareEncryptedAndroidNotificationRescinds,
+  prepareEncryptedWebNotifications,
 };
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
@@ -19,10 +19,15 @@
 } from 'lib/shared/message-utils.js';
 import { messageSpecs } from 'lib/shared/messages/message-specs.js';
 import { notifTextsForMessageInfo } from 'lib/shared/notif-utils.js';
+import { isStaff } from 'lib/shared/staff-utils.js';
 import {
   rawThreadInfoFromServerThreadInfo,
   threadInfoFromRawThreadInfo,
 } from 'lib/shared/thread-utils.js';
+import {
+  NEXT_CODE_VERSION,
+  hasMinCodeVersion,
+} from 'lib/shared/version-utils.js';
 import type { Platform, PlatformDetails } from 'lib/types/device-types.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
 import {
@@ -31,7 +36,6 @@
 } from 'lib/types/message-types.js';
 import { rawMessageInfoValidator } from 'lib/types/message-types.js';
 import type {
-  WebNotification,
   WNSNotification,
   ResolvedNotifTexts,
 } from 'lib/types/notif-types.js';
@@ -39,12 +43,14 @@
 import type { ServerThreadInfo, ThreadInfo } from 'lib/types/thread-types.js';
 import { updateTypes } from 'lib/types/update-types-enum.js';
 import { type GlobalUserInfo } from 'lib/types/user-types.js';
+import { isDev } from 'lib/utils/dev-utils.js';
 import { promiseAll } from 'lib/utils/promises.js';
 import { tID, tPlatformDetails, tShape } from 'lib/utils/validation-utils.js';
 
 import {
   prepareEncryptedIOSNotifications,
   prepareEncryptedAndroidNotifications,
+  prepareEncryptedWebNotifications,
 } from './crypto.js';
 import { getAPNsNotificationTopic } from './providers.js';
 import { rescindPushNotifs } from './rescind.js';
@@ -52,6 +58,7 @@
   NotificationTargetDevice,
   TargetedAPNsNotification,
   TargetedAndroidNotification,
+  TargetedWebNotification,
 } from './types.js';
 import {
   apnPush,
@@ -356,14 +363,18 @@
       };
 
       const deliveryPromise: Promise<PushResult> = (async () => {
-        const notification = await prepareWebNotification({
-          notifTexts,
-          threadID: threadInfo.id,
-          unreadCount,
-          platformDetails,
-        });
-        const deviceTokens = devices.map(({ deviceToken }) => deviceToken);
-        return await sendWebNotification(notification, deviceTokens, {
+        const targetedNotifications = await prepareWebNotification(
+          userID,
+          {
+            notifTexts,
+            threadID: threadInfo.id,
+            unreadCount,
+            platformDetails,
+          },
+          devices,
+        );
+
+        return await sendWebNotifications(targetedNotifications, {
           ...notificationInfo,
           codeVersion,
           stateVersion,
@@ -1053,8 +1064,10 @@
   platformDetails: tPlatformDetails,
 });
 async function prepareWebNotification(
+  userID: string,
   inputData: WebNotifInputData,
-): Promise<WebNotification> {
+  devices: $ReadOnlyArray<NotificationTargetDevice>,
+): Promise<$ReadOnlyArray<TargetedWebNotification>> {
   const convertedData = validateOutput(
     inputData.platformDetails,
     webNotifInputDataValidator,
@@ -1069,7 +1082,18 @@
     id,
     threadID,
   };
-  return notification;
+
+  const isStaffOrDev = isStaff(userID) || isDev;
+  const shouldBeEncrypted =
+    hasMinCodeVersion(convertedData.platformDetails, {
+      web: NEXT_CODE_VERSION,
+    }) && isStaffOrDev;
+
+  if (!shouldBeEncrypted) {
+    return devices.map(({ deviceToken }) => ({ deviceToken, notification }));
+  }
+
+  return prepareEncryptedWebNotifications(devices, notification);
 }
 
 type WNSNotifInputData = {
@@ -1272,18 +1296,17 @@
   +delivery: WebDelivery,
   +invalidTokens?: $ReadOnlyArray<string>,
 };
-async function sendWebNotification(
-  notification: WebNotification,
-  deviceTokens: $ReadOnlyArray<string>,
+async function sendWebNotifications(
+  targetedNotifications: $ReadOnlyArray<TargetedWebNotification>,
   notificationInfo: NotificationInfo,
 ): Promise<WebResult> {
   const { source, codeVersion, stateVersion } = notificationInfo;
 
-  const response = await webPush({
-    notification,
-    deviceTokens,
-  });
+  const response = await webPush(targetedNotifications);
 
+  const deviceTokens = targetedNotifications.map(
+    ({ deviceToken }) => deviceToken,
+  );
   const delivery: WebDelivery = {
     source,
     deviceType: 'web',
diff --git a/keyserver/src/push/types.js b/keyserver/src/push/types.js
--- a/keyserver/src/push/types.js
+++ b/keyserver/src/push/types.js
@@ -2,6 +2,8 @@
 
 import apn from '@parse/node-apn';
 
+import type { WebNotification } from 'lib/types/notif-types.js';
+
 export type TargetedAPNsNotification = {
   +notification: apn.Notification,
   +deviceToken: string,
@@ -55,6 +57,11 @@
   +deviceToken: string,
 };
 
+export type TargetedWebNotification = {
+  +notification: WebNotification,
+  +deviceToken: string,
+};
+
 export type NotificationTargetDevice = {
   +cookieID: string,
   +deviceToken: string,
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
@@ -11,10 +11,7 @@
 
 import blobService from 'lib/facts/blob-service.js';
 import type { PlatformDetails } from 'lib/types/device-types.js';
-import type {
-  WebNotification,
-  WNSNotification,
-} from 'lib/types/notif-types.js';
+import type { WNSNotification } from 'lib/types/notif-types.js';
 import { threadSubscriptions } from 'lib/types/subscription-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import { toBase64URL } from 'lib/utils/base64.js';
@@ -32,6 +29,7 @@
 import type {
   TargetedAPNsNotification,
   TargetedAndroidNotification,
+  TargetedWebNotification,
 } from './types.js';
 import { dbQuery, SQL } from '../database/database.js';
 import { generateKey, encrypt } from '../utils/aes-crypto-utils.js';
@@ -239,30 +237,31 @@
   +errors?: $ReadOnlyArray<WebPushError>,
   +invalidTokens?: $ReadOnlyArray<string>,
 };
-async function webPush({
-  notification,
-  deviceTokens,
-}: {
-  +notification: WebNotification,
-  +deviceTokens: $ReadOnlyArray<string>,
-}): Promise<WebPushResult> {
+async function webPush(
+  targetedNotifications: $ReadOnlyArray<TargetedWebNotification>,
+): Promise<WebPushResult> {
   await ensureWebPushInitialized();
-  const notificationString = JSON.stringify(notification);
 
   const pushResults = await Promise.all(
-    deviceTokens.map(async deviceTokenString => {
-      const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString);
-      try {
-        await webpush.sendNotification(deviceToken, notificationString);
-      } catch (error) {
-        return { error };
-      }
-      return {};
-    }),
+    targetedNotifications.map(
+      async ({ notification, deviceToken: deviceTokenString }) => {
+        const deviceToken: PushSubscriptionJSON = JSON.parse(deviceTokenString);
+        const notificationString = JSON.stringify(notification);
+        try {
+          await webpush.sendNotification(deviceToken, notificationString);
+        } catch (error) {
+          return { error };
+        }
+        return {};
+      },
+    ),
   );
 
   const errors = [];
   const invalidTokens = [];
+  const deviceTokens = targetedNotifications.map(
+    ({ deviceToken }) => deviceToken,
+  );
   for (let i = 0; i < pushResults.length; i++) {
     const pushResult = pushResults[i];
     if (pushResult.error) {
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
@@ -26,15 +26,29 @@
     prefix: t.maybe(t.String),
   });
 
-export type WebNotification = {
+export type PlainTextWebNotificationPayload = {
   +body: string,
   +prefix?: string,
   +title: string,
   +unreadCount: number,
-  +id: string,
   +threadID: string,
+  +encryptionFailed?: '1',
+};
+
+export type PlainTextWebNotification = {
+  +id: string,
+  ...PlainTextWebNotificationPayload,
 };
 
+export type EncryptedWebNotification = {
+  +id: string,
+  +encryptedPayload: string,
+};
+
+export type WebNotification =
+  | PlainTextWebNotification
+  | EncryptedWebNotification;
+
 export type WNSNotification = {
   +body: string,
   +prefix?: string,
diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js
--- a/web/push-notif/service-worker.js
+++ b/web/push-notif/service-worker.js
@@ -1,6 +1,6 @@
 // @flow
 
-import type { WebNotification } from 'lib/types/notif-types.js';
+import type { PlainTextWebNotification } from 'lib/types/notif-types.js';
 import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js';
 import { ashoatKeyserverID } from 'lib/utils/validation-utils.js';
 
@@ -23,7 +23,7 @@
 });
 
 self.addEventListener('push', (event: PushEvent) => {
-  const data: WebNotification = event.data.json();
+  const data: PlainTextWebNotification = event.data.json();
 
   event.waitUntil(
     (async () => {