diff --git a/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml b/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
index 6e61ec57d3d8..a3c895456d6f 100644
--- a/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
+++ b/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
@@ -23,6 +23,7 @@
 				<value>AssignmentService</value>
 				<value>AssessmentEntityProducer</value>
 				<value>ContentHostingService</value>
+				<value>conversations</value>
 				<value>CalendarService</value>
 				<value>ChatEntityProducer</value>
 				<value>DiscussionService</value>
diff --git a/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java b/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
index 5cd548e6db4a..1194a04dd2a7 100644
--- a/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
+++ b/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
@@ -34,8 +34,9 @@
 import org.sakaiproject.conversations.api.model.Tag;
 import org.sakaiproject.conversations.api.model.ConversationsTopic;
 import org.sakaiproject.entity.api.Entity;
+import org.sakaiproject.entity.api.EntityProducer;
 
-public interface ConversationsService {
+public interface ConversationsService extends EntityProducer {
 
     public static final String TOOL_ID = "sakai.conversations";
     public static final String REFERENCE_ROOT = Entity.SEPARATOR + "conversations";
diff --git a/conversations/impl/pom.xml b/conversations/impl/pom.xml
index ab16cb493880..d0b6af59f8f1 100644
--- a/conversations/impl/pom.xml
+++ b/conversations/impl/pom.xml
@@ -129,6 +129,11 @@
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-lang3</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.sakaiproject.common</groupId>
+      <artifactId>archive-api</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.opensearch</groupId>
       <artifactId>opensearch</artifactId>
diff --git a/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java b/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
index a62191fc6c03..df8d141949b6 100644
--- a/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
+++ b/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
@@ -32,8 +32,13 @@
 import java.util.Observer;
 import java.util.Optional;
 import java.util.Set;
+import java.util.Stack;
 import java.util.stream.Collectors;
 
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
 import org.sakaiproject.api.app.scheduler.ScheduledInvocationManager;
 import org.sakaiproject.authz.api.AuthzGroup;
 import org.sakaiproject.authz.api.AuthzGroupService;
@@ -85,7 +90,6 @@
 import org.sakaiproject.conversations.api.repository.TopicStatusRepository;
 import org.sakaiproject.entity.api.Entity;
 import org.sakaiproject.entity.api.EntityManager;
-import org.sakaiproject.entity.api.EntityProducer;
 import org.sakaiproject.entity.api.Reference;
 import org.sakaiproject.event.api.Event;
 import org.sakaiproject.event.api.EventTrackingService;
@@ -135,7 +139,7 @@
 @Slf4j
 @Setter
 @Transactional
-public class ConversationsServiceImpl implements ConversationsService, EntityProducer, EntityTransferrer, Observer {
+public class ConversationsServiceImpl implements ConversationsService, EntityTransferrer, Observer {
 
     private AuthzGroupService authzGroupService;
 
@@ -423,7 +427,7 @@ public Optional<String> getCommentPortalUrl(String commentId) {
     @Transactional
     public TopicTransferBean saveTopic(final TopicTransferBean topicBean, boolean sendMessage) throws ConversationsPermissionsException {
 
-        String currentUserId = getCheckedCurrentUserId();
+        String currentUserId = StringUtils.isNotBlank(topicBean.creator) ? topicBean.creator : getCheckedCurrentUserId();
 
         String siteRef = siteService.siteReference(topicBean.siteId);
 
@@ -2540,6 +2544,7 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
         return traversalMap;
     }
 
+    @Override
     public Map<String, String> transferCopyEntities(String fromContext, String toContext, List<String> ids, List<String> transferOptions, boolean cleanup) {
 
         if (cleanup) {
@@ -2556,6 +2561,108 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
         return transferCopyEntities(fromContext, toContext, ids, transferOptions);
     }
 
+    @Override
+    public boolean willArchiveMerge() {
+        return true;
+    }
+
+    @Override
+    public String getLabel() {
+        return "conversations";
+    }
+
+    @Override
+    public String archive(String siteId, Document doc, Stack<Element> stack, String archivePath, List<Reference> attachments) {
+
+        StringBuilder results = new StringBuilder();
+        results.append("begin archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
+
+        Element element = doc.createElement(getLabel());
+        stack.peek().appendChild(element);
+        stack.push(element);
+
+        Element topicsEl = doc.createElement("topics");
+        element.appendChild(topicsEl);
+
+        topicRepository.findBySiteId(siteId).stream().sorted((t1, t2) -> t1.getTitle().compareTo(t2.getTitle())).forEach(topic -> {
+
+            Element topicEl = doc.createElement("topic");
+            topicsEl.appendChild(topicEl);
+            topicEl.setAttribute("title", topic.getTitle());
+            topicEl.setAttribute("type", topic.getType().name());
+            topicEl.setAttribute("post-before-viewing", Boolean.toString(topic.getMustPostBeforeViewing()));
+            topicEl.setAttribute("allow-anonymous-posts", Boolean.toString(topic.getAllowAnonymousPosts()));
+            topicEl.setAttribute("pinned", Boolean.toString(topic.getPinned()));
+            topicEl.setAttribute("draft", Boolean.toString(topic.getDraft()));
+            topicEl.setAttribute("visibility", topic.getVisibility().name());
+            topicEl.setAttribute("creator", topic.getMetadata().getCreator());
+            topicEl.setAttribute("created", Long.toString(topic.getMetadata().getCreated().getEpochSecond()));
+
+            Element messageEl = doc.createElement("message");
+            messageEl.appendChild(doc.createCDATASection(topic.getMessage()));
+            topicEl.appendChild(messageEl);
+        });
+
+        results.append("completed archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
+        return results.toString();
+    }
+
+    @Override
+    public String merge(String toSiteId, Element root, String archivePath, String fromSiteId, Map<String, String> attachmentNames, Map<String, String> userIdTrans, Set<String> userListAllowImport) {
+
+        StringBuilder results = new StringBuilder();
+        results.append("begin merging ").append(getLabel()).append(" for site ").append(toSiteId).append(System.lineSeparator());
+
+        if (!root.getTagName().equals(getLabel())) {
+            log.warn("Tried to merge a non <{}> xml document", getLabel());
+            return "Invalid xml document";
+        }
+
+        Set<String> currentTitles = topicRepository.findBySiteId(toSiteId)
+            .stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+        NodeList topicNodes = root.getElementsByTagName("topic");
+
+        Instant now = Instant.now();
+
+        for (int i = 0; i < topicNodes.getLength(); i++) {
+
+            Element topicEl = (Element) topicNodes.item(i);
+            String title = topicEl.getAttribute("title");
+
+            if (currentTitles.contains(title)) {
+                log.debug("Topic \"{}\" already exists in site {}. Skipping merge ...", title, toSiteId);
+                continue;
+            }
+
+            TopicTransferBean topicBean = new TopicTransferBean();
+            topicBean.siteId = toSiteId;
+            topicBean.title = title;
+            topicBean.type = topicEl.getAttribute("type");
+            topicBean.created = now;
+            topicBean.mustPostBeforeViewing = Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing"));
+            topicBean.anonymous = Boolean.parseBoolean(topicEl.getAttribute("anonymous"));
+            topicBean.allowAnonymousPosts = Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts"));
+            topicBean.draft = Boolean.parseBoolean(topicEl.getAttribute("draft"));
+            topicBean.pinned = Boolean.parseBoolean(topicEl.getAttribute("pinned"));
+            topicBean.visibility = topicEl.getAttribute("visibility");
+
+            NodeList messageNodes = topicEl.getElementsByTagName("message");
+            if (messageNodes.getLength() == 1) {
+                topicBean.message = ((Element) messageNodes.item(0)).getFirstChild().getNodeValue();
+            }
+
+            try {
+                saveTopic(topicBean, false);
+            } catch (Exception e) {
+                log.warn("Failed to merge topic \"{}\": {}", topicBean.title, e.toString());
+            }
+        }
+
+        return "";
+    }
+
+    @Override
     public boolean parseEntityReference(String referenceString, Reference ref) {
 
         if (referenceString.startsWith(REFERENCE_ROOT)) {
diff --git a/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java b/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
index be3be75adef7..71f4d61e93f9 100644
--- a/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
+++ b/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
@@ -15,7 +15,7 @@
  */
 package org.sakaiproject.conversations.impl;
 
-import org.junit.Assume;
+import org.sakaiproject.archive.api.ArchiveService;
 import org.sakaiproject.authz.api.AuthzGroup;
 import org.sakaiproject.authz.api.AuthzGroupService;
 import org.sakaiproject.authz.api.SecurityService;
@@ -56,6 +56,7 @@
 import org.sakaiproject.user.api.UserDirectoryService;
 import org.sakaiproject.user.api.UserNotDefinedException;
 import org.sakaiproject.util.ResourceLoader;
+import org.sakaiproject.util.Xml;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.context.ContextConfiguration;
@@ -76,16 +77,20 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.Stack;
 import java.util.stream.Collectors;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 
-import static org.mockito.Mockito.*;
-
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
 
 import lombok.extern.slf4j.Slf4j;
 
+import static org.mockito.Mockito.*;
 import static org.junit.Assert.*;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -1968,6 +1973,163 @@ public void grading() {
         assertNull(savedBean.gradingItemId);
     }
 
+    @Test
+    public void archive() {
+
+        switchToInstructor(null);
+
+        String title1 = "Topic 1";
+        TopicTransferBean topic1 = new TopicTransferBean();
+        topic1.aboutReference = site1Ref;
+        topic1.title = title1;
+        topic1.message = "<strong>Something about topic1</strong>";
+        topic1.siteId = site1Id;
+        topic1 = saveTopic(topic1);
+
+        String title2 = "Topic 2";
+        TopicTransferBean topic2 = new TopicTransferBean();
+        topic2.aboutReference = site1Ref;
+        topic2.title = title2;
+        topic2.siteId = site1Id;
+        topic2 = saveTopic(topic2);
+
+        String title3 = "Topic 3";
+        TopicTransferBean topic3 = new TopicTransferBean();
+        topic3.aboutReference = site1Ref;
+        topic3.title = title3;
+        topic3.siteId = site1Id;
+        topic3 = saveTopic(topic3);
+
+        String title4 = "Topic 4";
+        TopicTransferBean topic4 = new TopicTransferBean();
+        topic4.aboutReference = site1Ref;
+        topic4.title = title4;
+        topic4.siteId = site1Id;
+        topic4 = saveTopic(topic4);
+
+        TopicTransferBean[] topicBeans = new TopicTransferBean[] { topic1, topic2, topic3, topic4 };
+
+        Document doc = Xml.createDocument();
+        Stack<Element> stack = new Stack<>();
+
+        Element root = doc.createElement("archive");
+        doc.appendChild(root);
+        root.setAttribute("source", site1Id);
+        root.setAttribute("xmlns:sakai", ArchiveService.SAKAI_ARCHIVE_NS);
+        root.setAttribute("xmlns:CHEF", ArchiveService.SAKAI_ARCHIVE_NS.concat("CHEF"));
+        root.setAttribute("xmlns:DAV", ArchiveService.SAKAI_ARCHIVE_NS.concat("DAV"));
+        stack.push(root);
+
+        assertEquals(1, stack.size());
+
+        String results = conversationsService.archive(site1Id, doc, stack, "", null);
+
+        assertEquals(2, stack.size());
+
+        NodeList conversationsNode = root.getElementsByTagName(conversationsService.getLabel());
+        assertEquals(1, conversationsNode.getLength());
+
+        NodeList topicsNode = ((Element) conversationsNode.item(0)).getElementsByTagName("topics");
+        assertEquals(1, topicsNode.getLength());
+
+        NodeList topicNodes = ((Element) topicsNode.item(0)).getElementsByTagName("topic");
+        assertEquals(topicBeans.length, topicNodes.getLength());
+
+        for (int i = 0; i < topicNodes.getLength(); i++) {
+            Element topicEl = (Element) topicNodes.item(i);
+            assertEquals(topicBeans[i].title, topicEl.getAttribute("title"));
+            assertEquals(topicBeans[i].type, topicEl.getAttribute("type"));
+            assertEquals(topicBeans[i].anonymous, Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
+            assertEquals(topicBeans[i].allowAnonymousPosts, Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts")));
+            assertEquals(topicBeans[i].pinned, Boolean.parseBoolean(topicEl.getAttribute("pinned")));
+            assertEquals(topicBeans[i].draft, Boolean.parseBoolean(topicEl.getAttribute("draft")));
+            assertEquals(topicBeans[i].visibility, topicEl.getAttribute("visibility"));
+            assertEquals(topicBeans[i].creator, topicEl.getAttribute("creator"));
+            assertEquals(topicBeans[i].created.getEpochSecond(), Long.parseLong(topicEl.getAttribute("created")));
+
+            NodeList messageNodes = topicEl.getElementsByTagName("message");
+            assertEquals(1, messageNodes.getLength());
+
+            assertEquals(topicBeans[i].message, ((Element) messageNodes.item(0)).getFirstChild().getNodeValue());
+        }
+    }
+
+    @Test
+    public void merge() {
+
+        Document doc = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations.xml"));
+
+        Element root = doc.getDocumentElement();
+
+        String fromSite = root.getAttribute("source");
+        String toSite = "my-new-site";
+
+        String toSiteRef = "/site/" + toSite;
+        switchToInstructor(toSiteRef);
+
+        when(siteService.siteReference(toSite)).thenReturn(toSiteRef);
+
+        Element conversationsElement = doc.createElement("not-conversations");
+
+        conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+        assertEquals("Invalid xml document", conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null));
+
+        conversationsElement = (Element) root.getElementsByTagName(conversationsService.getLabel()).item(0);
+
+        conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+        NodeList topicNodes = ((Element) conversationsElement.getElementsByTagName("topics").item(0)).getElementsByTagName("topic");
+
+        List<ConversationsTopic> topics = topicRepository.findBySiteId(toSite);
+
+        assertEquals(topics.size(), topicNodes.getLength());
+
+        for (int i = 0; i < topicNodes.getLength(); i++) {
+
+            Element topicEl = (Element) topicNodes.item(i);
+
+            String title = topicEl.getAttribute("title");
+            Optional<ConversationsTopic> optTopic = topics.stream().filter(t -> t.getTitle().equals(title)).findAny();
+            assertTrue(optTopic.isPresent());
+
+            ConversationsTopic topic = optTopic.get();
+
+            assertEquals(topic.getType().name(), topicEl.getAttribute("type"));
+            assertEquals(topic.getPinned(), Boolean.parseBoolean(topicEl.getAttribute("pinned")));
+            assertEquals(topic.getAnonymous(), Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
+            assertEquals(topic.getDraft(), Boolean.parseBoolean(topicEl.getAttribute("draft")));
+            assertEquals(topic.getMustPostBeforeViewing(), Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing")));
+
+            NodeList messageNodes = topicEl.getElementsByTagName("message");
+            assertEquals(1, messageNodes.getLength());
+
+            assertEquals(topic.getMessage(), messageNodes.item(0).getFirstChild().getNodeValue());
+        }
+
+        Set<String> oldTitles = topics.stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+        // Now let's try and merge this set of rubrics. It has one with a different title, but the
+        // rest the same, so we should end up with only one rubric being added.
+        Document doc2 = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations2.xml"));
+
+        Element root2 = doc2.getDocumentElement();
+
+        conversationsElement = (Element) root2.getElementsByTagName(conversationsService.getLabel()).item(0);
+
+        conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+        String extraTitle = "Smurfs";
+
+        assertEquals(topics.size() + 1, topicRepository.findBySiteId(toSite).size());
+
+        Set<String> newTitles = topicRepository.findBySiteId(toSite)
+            .stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+        assertFalse(oldTitles.contains(extraTitle));
+        assertTrue(newTitles.contains(extraTitle));
+    }
+
     private TopicTransferBean saveTopic(TopicTransferBean topicBean) {
 
         try {
diff --git a/conversations/impl/src/test/resources/archive/conversations.xml b/conversations/impl/src/test/resources/archive/conversations.xml
new file mode 100644
index 000000000000..88ef43f84aac
--- /dev/null
+++ b/conversations/impl/src/test/resources/archive/conversations.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?><archive date="20241211103321736" server="sakai1" source="1c51551f-947d-438c-bcb6-e5598dd84585" system="Sakai 2.8" xmlns:CHEF="https://www.sakailms.org/xmlns/archive/CHEF" xmlns:DAV="https://www.sakailms.org/xmlns/archive/DAV" xmlns:sakai="https://www.sakailms.org/xmlns/archive/"><conversations><topics><topic allow-anonymous-posts="false" created="1733859568" creator="admin" draft="false" pinned="false" post-before-viewing="false" title="Are aliens real?" type="DISCUSSION" visibility="INSTRUCTORS"><message><![CDATA[<p>Let&#39;s discuss <strong>aliens</strong>, right here.</p>
+]]></message></topic><topic allow-anonymous-posts="true" created="1733859508" creator="admin" draft="false" pinned="true" post-before-viewing="false" title="How many angels can dance on the end of a pin?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>It&#39;s philosophy, innit?</p>
+]]></message></topic><topic allow-anonymous-posts="false" created="1733859443" creator="admin" draft="false" pinned="false" post-before-viewing="true" title="Where are the toilets?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>Does anybody know <strong>where&nbsp;</strong>the toilets actually are?</p>
+]]></message></topic><topic allow-anonymous-posts="false" created="1733913178" creator="5d525dc9-5eb8-4afc-9294-061e7fbec373" draft="false" pinned="true" post-before-viewing="true" title="let's talk sports" type="DISCUSSION" visibility="SITE"><message><![CDATA[<p>sporting <strong>stuff</strong></p>
+]]></message></topic></topics></conversations></archive>
\ No newline at end of file
diff --git a/conversations/impl/src/test/resources/archive/conversations2.xml b/conversations/impl/src/test/resources/archive/conversations2.xml
new file mode 100644
index 000000000000..548cf7137aca
--- /dev/null
+++ b/conversations/impl/src/test/resources/archive/conversations2.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<archive date="20241210195221486" server="sakai1" source="1c51551f-947d-438c-bcb6-e5598dd84585" system="Sakai 2.8" xmlns:CHEF="https://www.sakailms.org/xmlns/archive/CHEF" xmlns:DAV="https://www.sakailms.org/xmlns/archive/DAV" xmlns:sakai="https://www.sakailms.org/xmlns/archive/"><conversations><topics><topic allow-anonymous-posts="false" created="1733859568" creator="admin" draft="false" pinned="false" post-before-viewing="false" title="Smurfs" type="DISCUSSION" visibility="INSTRUCTORS"><message><![CDATA[<p>Let&#39;s discuss <strong>aliens</strong>, right here.</p>
+]]></message></topic><topic allow-anonymous-posts="true" created="1733859508" creator="admin" draft="false" pinned="true" post-before-viewing="false" title="How many angels can dance on the end of a pin?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>It&#39;s philosophy, innit?</p>
+]]></message></topic><topic allow-anonymous-posts="false" created="1733859443" creator="admin" draft="false" pinned="false" post-before-viewing="true" title="Where are the toilets?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>Does anybody know <strong>where&nbsp;</strong>the toilets actually are?</p>
+]]></message></topic></topics></conversations></archive>