diff --git a/lib/actions/holder-actions.js b/lib/actions/holder-actions.js
--- a/lib/actions/holder-actions.js
+++ b/lib/actions/holder-actions.js
@@ -1,10 +1,24 @@
 // @flow
 
+import invariant from 'invariant';
+import * as React from 'react';
+
 import blobService from '../facts/blob-service.js';
-import type { AuthMetadata } from '../shared/identity-client-context.js';
-import type { BlobHashAndHolder } from '../types/holder-types.js';
+import {
+  type AuthMetadata,
+  IdentityClientContext,
+} from '../shared/identity-client-context.js';
+import type {
+  BlobHashAndHolder,
+  BlobOperation,
+} from '../types/holder-types.js';
 import { toBase64URL } from '../utils/base64.js';
-import { makeBlobServiceEndpointURL } from '../utils/blob-service.js';
+import {
+  makeBlobServiceEndpointURL,
+  generateBlobHolder,
+} from '../utils/blob-service.js';
+import { useDispatchActionPromise } from '../utils/redux-promise-utils.js';
+import { useSelector } from '../utils/redux-utils.js';
 import { createDefaultHTTPRequestHeaders } from '../utils/services-utils.js';
 
 type MultipleBlobHolders = $ReadOnlyArray<BlobHashAndHolder>;
@@ -118,4 +132,69 @@
   return { added, notAdded, removed, notRemoved };
 }
 
-export { processHoldersAction };
+function useProcessBlobHolders(): (
+  blobOperations: $ReadOnlyArray<BlobOperation>,
+) => Promise<void> {
+  const identityContext = React.useContext(IdentityClientContext);
+  const getAuthMetadata = identityContext?.getAuthMetadata;
+
+  const storedHolders = useSelector(state => state.holderStore.storedHolders);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  return React.useCallback(
+    async (ops: $ReadOnlyArray<BlobOperation>) => {
+      if (ops.length === 0) {
+        return;
+      }
+
+      invariant(getAuthMetadata, 'Identity context not set');
+      const authMetadata = await getAuthMetadata();
+
+      const holdersToAdd = ops
+        .map(({ blobHash, type }) => {
+          const status = storedHolders[blobHash]?.status;
+          if (
+            type !== 'establish_holder' ||
+            status === 'ESTABLISHED' ||
+            status === 'PENDING_ESTABLISHMENT'
+          ) {
+            return null;
+          }
+          return {
+            blobHash,
+            holder: generateBlobHolder(authMetadata.deviceID),
+          };
+        })
+        .filter(Boolean);
+
+      const holdersToRemove = ops
+        .map(({ blobHash, type }) => {
+          const holderInfo = storedHolders[blobHash];
+          if (
+            !holderInfo ||
+            type !== 'remove_holder' ||
+            holderInfo.status === 'PENDING_REMOVAL'
+          ) {
+            return null;
+          }
+          return { blobHash, holder: holderInfo.holder };
+        })
+        .filter(Boolean);
+
+      const input = {
+        holdersToAdd,
+        holdersToRemove,
+      };
+      const promise = processHoldersAction(input, authMetadata);
+      void dispatchActionPromise(
+        processHoldersActionTypes,
+        promise,
+        undefined,
+        input,
+      );
+    },
+    [dispatchActionPromise, getAuthMetadata, storedHolders],
+  );
+}
+
+export { useProcessBlobHolders };
diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js
--- a/lib/shared/dm-ops/process-dm-ops.js
+++ b/lib/shared/dm-ops/process-dm-ops.js
@@ -14,6 +14,7 @@
   dmOperationSpecificationTypes,
   type OutboundComposableDMOperationSpecification,
 } from './dm-op-utils.js';
+import { useProcessBlobHolders } from '../../actions/holder-actions.js';
 import {
   processNewUserIDsActionType,
   useFindUserIdentities,
@@ -71,6 +72,7 @@
   const dispatchWithMetadata = useDispatchWithMetadata();
   const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs);
   const currentUserInfo = useSelector(state => state.currentUserInfo);
+  const processBlobHolders = useProcessBlobHolders();
 
   const dispatch = useDispatch();
 
@@ -220,11 +222,30 @@
           return await dmOpSpec.notificationsCreationData(dmOp, utilities);
         })();
 
-      const [{ rawMessageInfos, updateInfos }, notificationsCreationData] =
-        await Promise.all([
-          dmOpSpec.processDMOperation(dmOp, utilities),
-          notificationsCreationDataPromise,
-        ]);
+      const [
+        { rawMessageInfos, updateInfos, blobOps },
+        notificationsCreationData,
+      ] = await Promise.all([
+        dmOpSpec.processDMOperation(dmOp, utilities),
+        notificationsCreationDataPromise,
+      ]);
+
+      const holderOps = blobOps
+        .map(({ dmOpType, ...holderOp }) => {
+          if (
+            (dmOpType === 'inbound_only' &&
+              dmOperationSpecification.type ===
+                dmOperationSpecificationTypes.OUTBOUND) ||
+            (dmOpType === 'outbound_only' &&
+              dmOperationSpecification.type ===
+                dmOperationSpecificationTypes.INBOUND)
+          ) {
+            return null;
+          }
+          return holderOp;
+        })
+        .filter(Boolean);
+      void processBlobHolders(holderOps);
 
       const { rawMessageInfos: allNewMessageInfos } =
         mergeUpdatesWithMessageInfos(rawMessageInfos, updateInfos);
@@ -318,6 +339,7 @@
       currentUserInfo,
       threadInfos,
       dispatch,
+      processBlobHolders,
     ],
   );
 }