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
@@ -84,6 +84,8 @@
 #ifdef EMSCRIPTEN
   virtual std::vector<WebThread> getAllThreadsWeb() const = 0;
   virtual void replaceThreadWeb(const WebThread &thread) const = 0;
+#else
+  virtual void createMainCompaction(std::string backupID) const = 0;
 #endif
 };
 
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
@@ -91,6 +91,7 @@
 #else
   static void clearSensitiveData();
   static void initialize(std::string &databasePath);
+  void createMainCompaction(std::string backupID) const override;
 #endif
 };
 
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
@@ -10,6 +10,7 @@
 
 #ifndef EMSCRIPTEN
 #include "CommSecureStore.h"
+#include "PlatformSpecificTools.h"
 #endif
 
 #define ACCOUNT_ID 1
@@ -570,9 +571,11 @@
   return false;
 }
 
-void set_encryption_key(sqlite3 *db) {
+void set_encryption_key(
+    sqlite3 *db,
+    const std::string &encryptionKey = SQLiteQueryExecutor::encryptionKey) {
   std::string set_encryption_key_query =
-      "PRAGMA key = \"x'" + SQLiteQueryExecutor::encryptionKey + "'\";";
+      "PRAGMA key = \"x'" + encryptionKey + "'\";";
 
   char *error_set_key;
   sqlite3_exec(
@@ -650,6 +653,14 @@
   trace_queries(db);
 }
 
+// This is a temporary solution. In future we want to keep
+// a separate table for blob hashes. Tracked on Linear:
+// https://linear.app/comm/issue/ENG-6261/introduce-blob-hash-table
+std::string blob_hash_from_blob_service_uri(const std::string &media_uri) {
+  static const std::string blob_service_prefix = "comm-blob-service://";
+  return media_uri.substr(blob_service_prefix.size());
+}
+
 bool file_exists(const std::string &file_path) {
   std::ifstream file(file_path.c_str());
   return file.good();
@@ -872,75 +883,9 @@
   return true;
 }
 
-void SQLiteQueryExecutor::migrate() {
-// We don't want to run `PRAGMA key = ...;`
-// on main web database. The context is here:
-// https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web
-#ifndef EMSCRIPTEN
-  validate_encryption();
-#endif
-
-  sqlite3 *db;
-  sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db);
-  on_database_open(db);
-
-  std::stringstream db_path;
-  db_path << "db path: " << SQLiteQueryExecutor::sqliteFilePath.c_str()
-          << std::endl;
-  Logger::log(db_path.str());
-
-  auto db_version = get_database_version(db);
-  std::stringstream version_msg;
-  version_msg << "db version: " << db_version << std::endl;
-  Logger::log(version_msg.str());
-
-  if (db_version == 0) {
-    auto db_created = set_up_database(db);
-    if (!db_created) {
-      sqlite3_close(db);
-      Logger::log("Database structure creation error.");
-      throw std::runtime_error("Database structure creation error");
-    }
-    Logger::log("Database structure created.");
-
-    sqlite3_close(db);
-    return;
-  }
-
-  for (const auto &[idx, migration] : migrations) {
-    const auto &[applyMigration, shouldBeInTransaction] = migration;
-
-    MigrationResult migrationResult;
-    if (shouldBeInTransaction) {
-      migrationResult = applyMigrationWithTransaction(db, applyMigration, idx);
-    } else {
-      migrationResult =
-          applyMigrationWithoutTransaction(db, applyMigration, idx);
-    }
-
-    if (migrationResult == MigrationResult::NOT_APPLIED) {
-      continue;
-    }
-
-    std::stringstream migration_msg;
-    if (migrationResult == MigrationResult::FAILURE) {
-      migration_msg << "migration " << idx << " failed." << std::endl;
-      Logger::log(migration_msg.str());
-      sqlite3_close(db);
-      throw std::runtime_error(migration_msg.str());
-    }
-    if (migrationResult == MigrationResult::SUCCESS) {
-      migration_msg << "migration " << idx << " succeeded." << std::endl;
-      Logger::log(migration_msg.str());
-    }
-  }
-
-  sqlite3_close(db);
-}
-
-auto &SQLiteQueryExecutor::getStorage() {
-  static auto storage = make_storage(
-      SQLiteQueryExecutor::sqliteFilePath,
+auto getEncryptedStorageAtPath(const std::string &databasePath) {
+  auto storage = make_storage(
+      databasePath,
       make_index("messages_idx_thread_time", &Message::thread, &Message::time),
       make_index("media_idx_container", &Media::container),
       make_table(
@@ -1021,6 +966,78 @@
   return storage;
 }
 
+void SQLiteQueryExecutor::migrate() {
+// We don't want to run `PRAGMA key = ...;`
+// on main web database. The context is here:
+// https://linear.app/comm/issue/ENG-6398/issues-with-sqlcipher-on-web
+#ifndef EMSCRIPTEN
+  validate_encryption();
+#endif
+
+  sqlite3 *db;
+  sqlite3_open(SQLiteQueryExecutor::sqliteFilePath.c_str(), &db);
+  on_database_open(db);
+
+  std::stringstream db_path;
+  db_path << "db path: " << SQLiteQueryExecutor::sqliteFilePath.c_str()
+          << std::endl;
+  Logger::log(db_path.str());
+
+  auto db_version = get_database_version(db);
+  std::stringstream version_msg;
+  version_msg << "db version: " << db_version << std::endl;
+  Logger::log(version_msg.str());
+
+  if (db_version == 0) {
+    auto db_created = set_up_database(db);
+    if (!db_created) {
+      sqlite3_close(db);
+      Logger::log("Database structure creation error.");
+      throw std::runtime_error("Database structure creation error");
+    }
+    Logger::log("Database structure created.");
+
+    sqlite3_close(db);
+    return;
+  }
+
+  for (const auto &[idx, migration] : migrations) {
+    const auto &[applyMigration, shouldBeInTransaction] = migration;
+
+    MigrationResult migrationResult;
+    if (shouldBeInTransaction) {
+      migrationResult = applyMigrationWithTransaction(db, applyMigration, idx);
+    } else {
+      migrationResult =
+          applyMigrationWithoutTransaction(db, applyMigration, idx);
+    }
+
+    if (migrationResult == MigrationResult::NOT_APPLIED) {
+      continue;
+    }
+
+    std::stringstream migration_msg;
+    if (migrationResult == MigrationResult::FAILURE) {
+      migration_msg << "migration " << idx << " failed." << std::endl;
+      Logger::log(migration_msg.str());
+      sqlite3_close(db);
+      throw std::runtime_error(migration_msg.str());
+    }
+    if (migrationResult == MigrationResult::SUCCESS) {
+      migration_msg << "migration " << idx << " succeeded." << std::endl;
+      Logger::log(migration_msg.str());
+    }
+  }
+
+  sqlite3_close(db);
+}
+
+auto &SQLiteQueryExecutor::getStorage() {
+  static auto storage =
+      getEncryptedStorageAtPath(SQLiteQueryExecutor::sqliteFilePath);
+  return storage;
+}
+
 SQLiteQueryExecutor::SQLiteQueryExecutor() {
   SQLiteQueryExecutor::migrate();
 }
@@ -1407,6 +1424,78 @@
   });
 }
 
+void SQLiteQueryExecutor::createMainCompaction(std::string backupID) const {
+  std::string finalBackupPath =
+      PlatformSpecificTools::getBackupFilePath(backupID, false);
+  std::string finalAttachmentsPath =
+      PlatformSpecificTools::getBackupFilePath(backupID, true);
+
+  std::string tempBackupPath = finalBackupPath + "_tmp";
+  std::string tempAttachmentsPath = finalAttachmentsPath + "_tmp";
+
+  if (file_exists(tempBackupPath)) {
+    Logger::log(
+        "Attempting to delete temporary backup file from previous backup "
+        "attempt.");
+    attempt_delete_file(
+        tempBackupPath,
+        "Failed to delete temporary backup file from previous backup attempt.");
+  }
+
+  if (file_exists(tempAttachmentsPath)) {
+    Logger::log(
+        "Attempting to delete temporary attachments file from previous backup "
+        "attempt.");
+    attempt_delete_file(
+        tempAttachmentsPath,
+        "Failed to delete temporary attachments file from previous backup "
+        "attempt.");
+  }
+
+  auto backupStorage = getEncryptedStorageAtPath(tempBackupPath);
+  auto backupObj =
+      SQLiteQueryExecutor::getStorage().make_backup_to(backupStorage);
+  int backupResult = backupObj.step(-1);
+
+  if (backupResult == SQLITE_BUSY || backupResult == SQLITE_LOCKED) {
+    throw std::runtime_error(
+        "Programmer error. Database in transaction during backup attempt.");
+  } else if (backupResult != SQLITE_DONE) {
+    std::stringstream error_message;
+    error_message << "Failed to create database backup. Details: "
+                  << sqlite3_errstr(backupResult);
+    throw std::runtime_error(error_message.str());
+  }
+  backupStorage.vacuum();
+
+  attempt_rename_file(
+      tempBackupPath,
+      finalBackupPath,
+      "Failed to rename complete temporary backup file to final backup file.");
+
+  std::ofstream tempAttachmentsFile(tempAttachmentsPath);
+  if (!tempAttachmentsFile.is_open()) {
+    throw std::runtime_error(
+        "Unable to create attachments file for backup id: " + backupID);
+  }
+
+  auto blobServiceURIRows = SQLiteQueryExecutor::getStorage().select(
+      columns(&Media::uri), where(like(&Media::uri, "comm-blob-service://%")));
+
+  for (const auto &blobServiceURIRow : blobServiceURIRows) {
+    std::string blobServiceURI = std::get<0>(blobServiceURIRow);
+    std::string blobHash = blob_hash_from_blob_service_uri(blobServiceURI);
+    tempAttachmentsFile << blobHash << "\n";
+  }
+  tempAttachmentsFile.close();
+
+  attempt_rename_file(
+      tempAttachmentsPath,
+      finalAttachmentsPath,
+      "Failed to rename complete temporary attachments file to final "
+      "attachments file.");
+}
+
 void SQLiteQueryExecutor::assign_encryption_key() {
   std::string encryptionKey = comm::crypto::Tools::generateRandomHexString(
       SQLiteQueryExecutor::sqlcipherEncryptionKeySize);
diff --git a/web/database/_generated/comm_query_executor.wasm b/web/database/_generated/comm_query_executor.wasm
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001

literal 0
Hc$@<O00001