diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
--- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
+++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
@@ -3,6 +3,7 @@
 #include "../CryptoTools/Persist.h"
 #include "entities/AuxUserInfo.h"
 #include "entities/CommunityInfo.h"
+#include "entities/DMOperation.h"
 #include "entities/Draft.h"
 #include "entities/EntryInfo.h"
 #include "entities/InboundP2PMessage.h"
@@ -190,6 +191,13 @@
       std::optional<std::string> messageIDCursor) const = 0;
   virtual std::vector<MessageEntity> getRelatedMessagesForSearch(
       const std::vector<std::string> &messageIDs) const = 0;
+  virtual void replaceDMOperation(const DMOperation &operation) const = 0;
+  virtual void removeAllDMOperations() const = 0;
+  virtual void
+  removeDMOperations(const std::vector<std::string> &ids) const = 0;
+  virtual std::vector<DMOperation> getDMOperations() const = 0;
+  virtual std::vector<DMOperation>
+  getDMOperationsByType(const std::string &operationType) const = 0;
   virtual ~DatabaseQueryExecutor() = default;
 
 #ifdef EMSCRIPTEN
diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
--- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
+++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
@@ -5,6 +5,7 @@
 #include "NativeSQLiteConnectionManager.h"
 #include "entities/AuxUserInfo.h"
 #include "entities/CommunityInfo.h"
+#include "entities/DMOperation.h"
 #include "entities/Draft.h"
 #include "entities/IntegrityThreadHash.h"
 #include "entities/KeyserverInfo.h"
@@ -204,6 +205,12 @@
       std::optional<std::string> messageIDCursor) const override;
   std::vector<MessageEntity> getRelatedMessagesForSearch(
       const std::vector<std::string> &messageIDs) const override;
+  void replaceDMOperation(const DMOperation &operation) const override;
+  void removeAllDMOperations() const override;
+  void removeDMOperations(const std::vector<std::string> &ids) const override;
+  std::vector<DMOperation> getDMOperations() const override;
+  std::vector<DMOperation>
+  getDMOperationsByType(const std::string &operationType) const override;
 
 #ifdef EMSCRIPTEN
   std::vector<WebThread> getAllThreadsWeb() const override;
diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
--- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
+++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
@@ -824,6 +824,19 @@
   return false;
 }
 
+bool create_dm_operations_table(sqlite3 *db) {
+  std::string query =
+      "CREATE TABLE IF NOT EXISTS dm_operations ("
+      "  id TEXT PRIMARY KEY,"
+      "  type TEXT NOT NULL,"
+      "  operation TEXT NOT NULL"
+      ");"
+      "CREATE INDEX IF NOT EXISTS dm_operations_idx_type"
+      "  ON dm_operations (type);";
+
+  return create_table(db, query, "dm_operations");
+}
+
 bool create_schema(sqlite3 *db) {
   char *error;
   int sidebarSourceTypeInt = static_cast<int>(MessageType::SIDEBAR_SOURCE);
@@ -998,6 +1011,12 @@
       "  tokenize = porter"
       ");"
 
+      "CREATE TABLE IF NOT EXISTS dm_operations ("
+      "  id TEXT PRIMARY KEY,"
+      "  type TEXT NOT NULL,"
+      "  operation TEXT NOT NULL"
+      ");"
+
       "CREATE INDEX IF NOT EXISTS media_idx_container"
       "  ON media (container);"
 
@@ -1008,7 +1027,10 @@
       "  ON messages (target_message, type, time);"
 
       "CREATE INDEX IF NOT EXISTS outbound_p2p_messages_idx_id_timestamp"
-      "  ON outbound_p2p_messages (device_id, timestamp);";
+      "  ON outbound_p2p_messages (device_id, timestamp);"
+
+      "CREATE INDEX IF NOT EXISTS dm_operations_idx_type"
+      "  ON dm_operations (type);";
 
   sqlite3_exec(db, query.c_str(), nullptr, nullptr, &error);
 
@@ -1261,7 +1283,8 @@
      {50, {create_message_search_table, true}},
      {51, {update_messages_idx_target_message_type_time, true}},
      {52, {recreate_inbound_p2p_messages_table, true}},
-     {53, {add_timestamps_column_to_threads_table, true}}}};
+     {53, {add_timestamps_column_to_threads_table, true}},
+     {54, {create_dm_operations_table, true}}}};
 
 enum class MigrationResult { SUCCESS, FAILURE, NOT_APPLIED };
 
@@ -2887,6 +2910,54 @@
   return this->processMessagesResults(preparedSQL);
 }
 
+void SQLiteQueryExecutor::replaceDMOperation(
+    const DMOperation &operation) const {
+  static std::string query =
+      "REPLACE INTO dm_operations (id, type, operation) "
+      "VALUES (?, ?, ?);";
+  replaceEntity<DMOperation>(
+      SQLiteQueryExecutor::getConnection(), query, operation);
+}
+
+void SQLiteQueryExecutor::removeAllDMOperations() const {
+  static std::string query = "DELETE FROM dm_operations;";
+  removeAllEntities(SQLiteQueryExecutor::getConnection(), query);
+}
+
+void SQLiteQueryExecutor::removeDMOperations(
+    const std::vector<std::string> &ids) const {
+  if (!ids.size()) {
+    return;
+  }
+
+  std::stringstream queryStream;
+  queryStream << "DELETE FROM dm_operations "
+                 "WHERE id IN "
+              << getSQLStatementArray(ids.size()) << ";";
+  removeEntitiesByKeys(
+      SQLiteQueryExecutor::getConnection(), queryStream.str(), ids);
+}
+
+std::vector<DMOperation> SQLiteQueryExecutor::getDMOperations() const {
+  static std::string query =
+      "SELECT id, type, operation "
+      "FROM dm_operations;";
+  return getAllEntities<DMOperation>(
+      SQLiteQueryExecutor::getConnection(), query);
+}
+
+std::vector<DMOperation> SQLiteQueryExecutor::getDMOperationsByType(
+    const std::string &operationType) const {
+  static std::string query =
+      "SELECT id, type, operation "
+      "FROM dm_operations "
+      "WHERE type = ?;";
+
+  std::vector<std::string> types{operationType};
+  return getAllEntitiesByPrimaryKeys<DMOperation>(
+      SQLiteQueryExecutor::getConnection(), query, types);
+}
+
 #ifdef EMSCRIPTEN
 std::vector<WebThread> SQLiteQueryExecutor::getAllThreadsWeb() const {
   auto threads = this->getAllThreads();
diff --git a/native/cpp/CommonCpp/DatabaseManagers/entities/DMOperation.h b/native/cpp/CommonCpp/DatabaseManagers/entities/DMOperation.h
new file mode 100644
--- /dev/null
+++ b/native/cpp/CommonCpp/DatabaseManagers/entities/DMOperation.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "SQLiteDataConverters.h"
+#include <sqlite3.h>
+#include <string>
+
+namespace comm {
+struct DMOperation {
+
+  std::string id;
+  std::string type;
+  std::string operation;
+
+  static DMOperation fromSQLResult(sqlite3_stmt *sqlRow, int idx) {
+    return DMOperation{
+        getStringFromSQLRow(sqlRow, idx),
+        getStringFromSQLRow(sqlRow, idx + 1),
+        getStringFromSQLRow(sqlRow, idx + 2)};
+  }
+
+  int bindToSQL(sqlite3_stmt *sql, int idx) const {
+    bindStringToSQL(id, sql, idx);
+    bindStringToSQL(type, sql, idx + 1);
+    return bindStringToSQL(operation, sql, idx + 2);
+  }
+};
+
+} // namespace comm
diff --git a/native/ios/Comm.xcodeproj/project.pbxproj b/native/ios/Comm.xcodeproj/project.pbxproj
--- a/native/ios/Comm.xcodeproj/project.pbxproj
+++ b/native/ios/Comm.xcodeproj/project.pbxproj
@@ -143,6 +143,7 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		0E02676D2D81EAD800788249 /* DMOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DMOperation.h; sourceTree = "<group>"; };
 		13B07F961A680F5B00A75B9A /* Comm.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Comm.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Comm/AppDelegate.h; sourceTree = "<group>"; };
 		13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Comm/AppDelegate.mm; sourceTree = "<group>"; };
@@ -551,6 +552,7 @@
 		71BE84442636A944002849D2 /* entities */ = {
 			isa = PBXGroup;
 			children = (
+				0E02676D2D81EAD800788249 /* DMOperation.h */,
 				816D2D5B2C480E9E001C0B67 /* MessageSearchResult.h */,
 				CB01F0C32B67F3970089E1F9 /* SQLiteStatementWrapper.cpp */,
 				CB01F0C12B67EF470089E1F9 /* SQLiteDataConverters.cpp */,
diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001

literal 0
Hc$@<O00001