diff --git a/lib/tunnelbroker/peer-to-peer-context.js b/lib/tunnelbroker/peer-to-peer-context.js
--- a/lib/tunnelbroker/peer-to-peer-context.js
+++ b/lib/tunnelbroker/peer-to-peer-context.js
@@ -14,6 +14,7 @@
   type DMOperationSpecification,
 } from '../shared/dm-ops/dm-op-utils.js';
 import {
+  type AuthMetadata,
   IdentityClientContext,
   type IdentityClientContextType,
 } from '../shared/identity-client-context.js';
@@ -36,6 +37,11 @@
     dmOpID: ?string,
   ) => void,
   +sendDMOperation: (op: DMOperationSpecification) => Promise<void>,
+  +broadcastEphemeralMessage: (
+    contentPayload: string,
+    recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>,
+    authMetadata: AuthMetadata,
+  ) => Promise<void>,
 };
 
 const PeerToPeerContext: React.Context<?PeerToPeerContextType> =
@@ -246,6 +252,69 @@
     [peerOlmSessionsCreator, identityContext, sendMessageToDevice],
   );
 
+  const broadcastEphemeralMessage = React.useCallback(
+    async (
+      contentPayload: string,
+      recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>,
+      authMetadata: AuthMetadata,
+    ) => {
+      const { userID: thisUserID, deviceID: thisDeviceID } = authMetadata;
+      if (!thisDeviceID || !thisUserID) {
+        throw new Error('No auth metadata');
+      }
+      const { olmAPI } = getConfig();
+      await olmAPI.initializeCryptoAccount();
+
+      // We want it distinct by device ID to avoid potentially creating
+      // multiple Olm sessions with the same device simultaneously.
+      const recipientsDistinctByDeviceID = [
+        ...new Map(recipients.map(item => [item.deviceID, item])).values(),
+      ];
+      const senderInfo = { deviceID: thisDeviceID, userID: thisUserID };
+      const promises = recipientsDistinctByDeviceID.map(async recipient => {
+        try {
+          const encryptedData = await olmAPI.encrypt(
+            contentPayload,
+            recipient.deviceID,
+          );
+          const encryptedMessage: EncryptedMessage = {
+            type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE,
+            senderInfo,
+            encryptedData,
+          };
+          await sendMessageToDevice({
+            deviceID: recipient.deviceID,
+            payload: JSON.stringify(encryptedMessage),
+          });
+        } catch {
+          try {
+            await peerOlmSessionsCreator(recipient.userID, recipient.deviceID);
+            const encryptedData = await olmAPI.encrypt(
+              contentPayload,
+              recipient.deviceID,
+            );
+            const encryptedMessage: EncryptedMessage = {
+              type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE,
+              senderInfo,
+              encryptedData,
+            };
+            await sendMessageToDevice({
+              deviceID: recipient.deviceID,
+              payload: JSON.stringify(encryptedMessage),
+            });
+          } catch (err) {
+            console.warn(
+              `Error sending Olm-encrypted message to device ${recipient.deviceID}:`,
+              err,
+            );
+          }
+        }
+      });
+      await Promise.all(promises);
+    },
+    [peerOlmSessionsCreator, sendMessageToDevice],
+  );
+
   React.useEffect(() => {
     const intervalID = setInterval(
       processOutboundMessages,
@@ -258,8 +327,9 @@
     () => ({
       processOutboundMessages,
       sendDMOperation,
+      broadcastEphemeralMessage,
     }),
-    [processOutboundMessages, sendDMOperation],
+    [broadcastEphemeralMessage, processOutboundMessages, sendDMOperation],
   );
 
   return (