diff --git a/src/account.h b/src/account.h
index 0b00e46b48f12da5540967e5e2a2e2a760127ebe..7ebd4fb4ec79905ddb69377e7ed181ebf62ad98c 100644
--- a/src/account.h
+++ b/src/account.h
@@ -302,6 +302,11 @@ public:
      */
     virtual void connectivityChanged() {};
 
+    virtual void onNewGitCommit(const std::string& /*peer*/,
+                                const std::string& /*deviceId*/,
+                                const std::string& /*conversationId*/,
+                                const std::string& /*commitId*/) {};
+
     /**
      * Helper function used to load the default codec order from the codec factory
      */
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
index a366ed602d516961a535d9a31e1789de0eb35c44..5c4479ec6e2fdc14fcb9e1ef44e5f8513f5954f1 100644
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -11,6 +11,7 @@ list (APPEND Source_Files__client
       "${CMAKE_CURRENT_SOURCE_DIR}/videomanager.cpp"
       "${CMAKE_CURRENT_SOURCE_DIR}/videomanager.h"
       "${CMAKE_CURRENT_SOURCE_DIR}/plugin_manager_interface.cpp"
+      "${CMAKE_CURRENT_SOURCE_DIR}/conversation_interface.cpp"
 )
 
 set (Source_Files__client ${Source_Files__client} PARENT_SCOPE)
\ No newline at end of file
diff --git a/src/client/Makefile.am b/src/client/Makefile.am
index 736a894eebc3fc37166d273048bc38749816546b..fc5ad392f0255b5736160caadce5ec966a560240 100644
--- a/src/client/Makefile.am
+++ b/src/client/Makefile.am
@@ -21,6 +21,7 @@ libclient_la_SOURCES = \
 	callmanager.cpp \
 	configurationmanager.cpp \
 	datatransfer.cpp \
+	conversation_interface.cpp \
 	$(PLUGIN_SRC) \
 	$(PRESENCE_SRC) \
 	$(VIDEO_SRC)
diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..75475ca86889c636662e4add749a84a6a892c2fe
--- /dev/null
+++ b/src/client/conversation_interface.cpp
@@ -0,0 +1,133 @@
+/*
+ *  Copyright (C) 2013-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "conversation_interface.h"
+
+#include <cerrno>
+#include <sstream>
+#include <cstring>
+
+#include "logger.h"
+#include "manager.h"
+#include "jamidht/jamiaccount.h"
+
+namespace DRing {
+
+std::string
+startConversation(const std::string& accountId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->startConversation();
+    return {};
+}
+
+void
+acceptConversationRequest(const std::string& accountId, const std::string& conversationId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        acc->acceptConversationRequest(conversationId);
+}
+
+void
+declineConversationRequest(const std::string& accountId, const std::string& conversationId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        acc->declineConversationRequest(conversationId);
+}
+
+bool
+removeConversation(const std::string& accountId, const std::string& conversationId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->removeConversation(conversationId);
+    return false;
+}
+
+std::vector<std::string>
+getConversations(const std::string& accountId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->getConversations();
+    return {};
+}
+
+std::vector<std::map<std::string, std::string>>
+getConversationRequests(const std::string& accountId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->getConversationRequests();
+    return {};
+}
+
+// Member management
+bool
+addConversationMember(const std::string& accountId,
+                      const std::string& conversationId,
+                      const std::string& contactUri)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->addConversationMember(conversationId, contactUri);
+    return false;
+}
+
+bool
+removeConversationMember(const std::string& accountId,
+                         const std::string& conversationId,
+                         const std::string& contactUri)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->removeConversationMember(conversationId, contactUri);
+    return false;
+}
+
+std::vector<std::map<std::string, std::string>>
+getConversationMembers(const std::string& accountId, const std::string& conversationId)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        return acc->getConversationMembers(conversationId);
+    return {};
+}
+
+// Message send/load
+void
+sendMessage(const std::string& accountId,
+            const std::string& conversationId,
+            const std::string& message,
+            const std::string& parent)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        acc->sendMessage(conversationId, message, parent);
+}
+
+void
+loadConversationMessages(const std::string& accountId,
+                         const std::string& conversationId,
+                         const std::string& fromMessage,
+                         size_t n)
+{
+    if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId))
+        acc->loadConversationMessages(conversationId, fromMessage, n);
+}
+
+} // namespace DRing
diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp
index d4e05aa3185b6cfae4aecb87b771ab070b0eb6e0..689f7812947d83bdbd0487346ebb69829655cc5a 100644
--- a/src/client/ring_signal.cpp
+++ b/src/client/ring_signal.cpp
@@ -125,6 +125,12 @@ getSignalHandlers()
         exported_callback<DRing::VideoSignal::DeviceAdded>(),
         exported_callback<DRing::VideoSignal::ParametersChanged>(),
 #endif
+
+        /* Conversation */
+        exported_callback<DRing::ConversationSignal::ConversationLoaded>(),
+        exported_callback<DRing::ConversationSignal::MessageReceived>(),
+        exported_callback<DRing::ConversationSignal::ConversationRequestReceived>(),
+        exported_callback<DRing::ConversationSignal::ConversationReady>(),
     };
 
     return handlers;
diff --git a/src/client/ring_signal.h b/src/client/ring_signal.h
index 9326d899af8faf36c449b8d96c132fba896f4841..2d75e0bac0226f56f689bba25903a87a0839ea20 100644
--- a/src/client/ring_signal.h
+++ b/src/client/ring_signal.h
@@ -26,6 +26,7 @@
 
 #include "callmanager_interface.h"
 #include "configurationmanager_interface.h"
+#include "conversation_interface.h"
 #include "presencemanager_interface.h"
 #include "datatransfer_interface.h"
 
diff --git a/src/dring/conversation_interface.h b/src/dring/conversation_interface.h
new file mode 100644
index 0000000000000000000000000000000000000000..890fdf3ac91b918587fdb689aaac072d018015f0
--- /dev/null
+++ b/src/dring/conversation_interface.h
@@ -0,0 +1,99 @@
+/*
+ *  Copyright (C) 2013-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#ifndef DRING_CONVERSATIONI_H
+#define DRING_CONVERSATIONI_H
+
+#include "def.h"
+
+#include <vector>
+#include <map>
+#include <string>
+
+#include "dring.h"
+
+namespace DRing {
+
+// Conversation management
+DRING_PUBLIC std::string startConversation(const std::string& accountId);
+DRING_PUBLIC void acceptConversationRequest(const std::string& accountId,
+                                            const std::string& conversationId);
+DRING_PUBLIC void declineConversationRequest(const std::string& accountId,
+                                             const std::string& conversationId);
+DRING_PUBLIC bool removeConversation(const std::string& accountId,
+                                     const std::string& conversationId);
+DRING_PUBLIC std::vector<std::string> getConversations(const std::string& accountId);
+DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationRequests(
+    const std::string& accountId);
+
+// Member management
+DRING_PUBLIC bool addConversationMember(const std::string& accountId,
+                                        const std::string& conversationId,
+                                        const std::string& contactUri);
+DRING_PUBLIC bool removeConversationMember(const std::string& accountId,
+                                           const std::string& conversationId,
+                                           const std::string& contactUri);
+DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationMembers(
+    const std::string& accountId, const std::string& conversationId);
+
+// Message send/load
+DRING_PUBLIC void sendMessage(const std::string& accountId,
+                              const std::string& conversationId,
+                              const std::string& message,
+                              const std::string& parent);
+DRING_PUBLIC void loadConversationMessages(const std::string& accountId,
+                                           const std::string& conversationId,
+                                           const std::string& fromMessage,
+                                           size_t n);
+
+struct DRING_PUBLIC ConversationSignal
+{
+    struct DRING_PUBLIC ConversationLoaded
+    {
+        constexpr static const char* name = "ConversationLoaded";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */,
+                             std::vector<std::map<std::string, std::string>> /*messages*/);
+    };
+    struct DRING_PUBLIC MessageReceived
+    {
+        constexpr static const char* name = "MessageReceived";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */,
+                             std::map<std::string, std::string> /*message*/);
+    };
+    struct DRING_PUBLIC ConversationRequestReceived
+    {
+        constexpr static const char* name = "ConversationRequestReceived";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */,
+                             std::map<std::string, std::string> /*metadatas*/);
+    };
+    struct DRING_PUBLIC ConversationReady
+    {
+        constexpr static const char* name = "ConversationReady";
+        using cb_type = void(const std::string& /*accountId*/,
+                             const std::string& /* conversationId */);
+    };
+};
+
+} // namespace DRing
+
+#endif // DRING_CONVERSATIONI_H
diff --git a/src/gittransport.cpp b/src/gittransport.cpp
index 6caedddf26d5c9ee93cefb5d020f551bb327ec9e..473e6053354cee760a74c5b07dd094d7a2c4f93b 100644
--- a/src/gittransport.cpp
+++ b/src/gittransport.cpp
@@ -47,7 +47,7 @@ generateRequest(git_buf* request, const std::string& cmd, const std::string_view
                  + cmd.size()                        /* followed by the command */
                  + 1                                 /* space */
                  + conversationId.size()             /* conversation */
-                 + nullSeparator.size()              /* \0 */
+                 + 1                                 /* \0 */
                  + HOST_TAG.size() + deviceId.size() /* device */
                  + nullSeparator.size() /* \0 */;
 
@@ -74,6 +74,7 @@ sendCmd(P2PStream* s)
     if ((res = s->socket->write(reinterpret_cast<const unsigned char*>(request.ptr),
                                 request.size,
                                 ec))) {
+        s->sent_command = 1;
         git_buf_free(&request);
         return res;
     }
diff --git a/src/jamidht/CMakeLists.txt b/src/jamidht/CMakeLists.txt
index 6a87b1cd7b1c54687534535f2b0e12a9959ea5fd..143342ccd01b7f58371e1e2c850c45813f11b666 100644
--- a/src/jamidht/CMakeLists.txt
+++ b/src/jamidht/CMakeLists.txt
@@ -17,6 +17,8 @@ list (APPEND Source_Files__jamidht
       "${CMAKE_CURRENT_SOURCE_DIR}/connectionmanager.h"
       "${CMAKE_CURRENT_SOURCE_DIR}/conversationrepository.cpp"
       "${CMAKE_CURRENT_SOURCE_DIR}/conversationrepository.h"
+      "${CMAKE_CURRENT_SOURCE_DIR}/conversation.cpp"
+      "${CMAKE_CURRENT_SOURCE_DIR}/conversation.h"
       "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transport.cpp"
       "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transport.h"
       "${CMAKE_CURRENT_SOURCE_DIR}/channeled_transfers.cpp"
diff --git a/src/jamidht/Makefile.am b/src/jamidht/Makefile.am
index 43eadf64b83733c2e8bd2eaf98aa9a8d6185f0c6..47b477c0101dda90a2338f5670d8c733e0c7ef94 100644
--- a/src/jamidht/Makefile.am
+++ b/src/jamidht/Makefile.am
@@ -19,6 +19,8 @@ libringacc_la_SOURCES = \
         channeled_transport.cpp \
         channeled_transfers.h \
         channeled_transfers.cpp \
+        conversation.h \
+        conversation.cpp \
         conversationrepository.h \
         conversationrepository.cpp \
         gitserver.h \
diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c5a88b358260f101cf517d3e1cfe60fa0fd61c44
--- /dev/null
+++ b/src/jamidht/conversation.cpp
@@ -0,0 +1,288 @@
+/*
+ *  Copyright (C) 2014-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#include "conversation.h"
+
+#include "fileutils.h"
+#include "jamiaccount.h"
+#include "conversationrepository.h"
+
+#include <json/json.h>
+
+namespace jami {
+
+class Conversation::Impl
+{
+public:
+    Impl(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId)
+        : account_(account)
+    {
+        if (conversationId.empty())
+            repository_ = ConversationRepository::createConversation(account);
+        else
+            repository_ = std::make_unique<ConversationRepository>(account, conversationId);
+        if (!repository_) {
+            throw std::logic_error("Couldn't create repository");
+        }
+    }
+
+    Impl(const std::weak_ptr<JamiAccount>& account,
+         const std::string& remoteDevice,
+         const std::string& conversationId)
+        : account_(account)
+    {
+        repository_ = ConversationRepository::cloneConversation(account,
+                                                                remoteDevice,
+                                                                conversationId);
+        if (!repository_) {
+            throw std::logic_error("Couldn't clone repository");
+        }
+    }
+    ~Impl() = default;
+
+    std::unique_ptr<ConversationRepository> repository_;
+    std::weak_ptr<JamiAccount> account_;
+    std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "",
+                                                                 const std::string& toMessage = "",
+                                                                 size_t n = 0);
+};
+
+std::vector<std::map<std::string, std::string>>
+Conversation::Impl::loadMessages(const std::string& fromMessage,
+                                 const std::string& toMessage,
+                                 size_t n)
+{
+    if (!repository_)
+        return {};
+    std::vector<ConversationCommit> convCommits;
+    if (toMessage.empty())
+        convCommits = repository_->logN(fromMessage, n);
+    else
+        convCommits = repository_->log(fromMessage, toMessage);
+    std::vector<std::map<std::string, std::string>> result = {};
+    for (const auto& commit : convCommits) {
+        auto authorDevice = commit.author.email;
+        auto cert = tls::CertificateStore::instance().getCertificate(authorDevice);
+        if (!cert && cert->issuer) {
+            JAMI_WARN("No author found for commit %s", commit.id.c_str());
+        }
+        auto authorId = cert->issuer->getId().toString();
+        std::string parents;
+        auto parentsSize = commit.parents.size();
+        for (auto i = 0; i < parentsSize; ++i) {
+            parents += commit.parents[i];
+            if (i != parentsSize - 1)
+                parents += ",";
+        }
+        std::string type {};
+        if (parentsSize > 1) {
+            type = "merge";
+        }
+        // TODO check diff for member
+        std::string body {};
+        if (type.empty()) {
+            std::string err;
+            Json::Value cm;
+            Json::CharReaderBuilder rbuilder;
+            auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+            if (reader->parse(commit.commit_msg.data(),
+                              commit.commit_msg.data() + commit.commit_msg.size(),
+                              &cm,
+                              &err)) {
+                type = cm["type"].asString();
+                body = cm["body"].asString();
+            } else {
+                JAMI_WARN("%s", err.c_str());
+            }
+        }
+        std::map<std::string, std::string> message {{"id", commit.id},
+                                                    {"parents", parents},
+                                                    {"author", authorId},
+                                                    {"type", type},
+                                                    {"body", body},
+                                                    {"timestamp", std::to_string(commit.timestamp)}};
+        result.emplace_back(message);
+    }
+    return result;
+}
+
+Conversation::Conversation(const std::weak_ptr<JamiAccount>& account,
+                           const std::string& conversationId)
+    : pimpl_ {new Impl {account, conversationId}}
+{}
+
+Conversation::Conversation(const std::weak_ptr<JamiAccount>& account,
+                           const std::string& remoteDevice,
+                           const std::string& conversationId)
+    : pimpl_ {new Impl {account, remoteDevice, conversationId}}
+{}
+
+Conversation::~Conversation() {}
+
+std::string
+Conversation::id() const
+{
+    return pimpl_->repository_ ? pimpl_->repository_->id() : "";
+}
+
+std::string
+Conversation::addMember(const std::string& contactUri)
+{
+    // Add member files and commit
+    return pimpl_->repository_->addMember(contactUri);
+}
+
+bool
+Conversation::removeMember(const std::string& contactUri)
+{
+    // TODO
+    return true;
+}
+
+std::vector<std::map<std::string, std::string>>
+Conversation::getMembers()
+{
+    std::vector<std::map<std::string, std::string>> result;
+    auto shared = pimpl_->account_.lock();
+    if (!shared)
+        return result;
+
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR
+                    + pimpl_->repository_->id();
+    auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins";
+    auto membersPath = repoPath + DIR_SEPARATOR_STR + "members";
+    for (const auto& certificate : fileutils::readDirectory(adminsPath)) {
+        if (certificate.find(".crt") == std::string::npos) {
+            JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str());
+            continue;
+        }
+        std::map<std::string, std::string>
+            details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())},
+                     {"role", "admin"}};
+        result.emplace_back(details);
+    }
+    for (const auto& certificate : fileutils::readDirectory(membersPath)) {
+        if (certificate.find(".crt") == std::string::npos) {
+            JAMI_WARN("Incorrect file found: %s/%s", membersPath.c_str(), certificate.c_str());
+            continue;
+        }
+        std::map<std::string, std::string>
+            details {{"uri", certificate.substr(0, certificate.size() - std::string(".crt").size())},
+                     {"role", "member"}};
+        result.emplace_back(details);
+    }
+
+    return result;
+}
+
+std::string
+Conversation::join()
+{
+    auto shared = pimpl_->account_.lock();
+    if (!shared)
+        return {};
+    return pimpl_->repository_->join();
+}
+
+bool
+Conversation::isMember(const std::string& uri, bool includeInvited)
+{
+    auto shared = pimpl_->account_.lock();
+    if (!shared)
+        return false;
+
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR
+                    + pimpl_->repository_->id();
+    auto invitedPath = repoPath + DIR_SEPARATOR_STR + "invited";
+    auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins";
+    auto membersPath = repoPath + DIR_SEPARATOR_STR + "members";
+    std::vector<std::string> pathsToCheck = {adminsPath, membersPath};
+    if (includeInvited)
+        pathsToCheck.emplace_back(invitedPath);
+    for (const auto& path : pathsToCheck) {
+        for (const auto& certificate : fileutils::readDirectory(path)) {
+            if (certificate.find(".crt") == std::string::npos) {
+                JAMI_WARN("Incorrect file found: %s/%s", path.c_str(), certificate.c_str());
+                continue;
+            }
+            auto crtUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
+            if (crtUri == uri)
+                return true;
+        }
+    }
+
+    return false;
+}
+
+std::string
+Conversation::sendMessage(const std::string& message,
+                          const std::string& type,
+                          const std::string& parent)
+{
+    Json::Value json;
+    json["body"] = message;
+    json["type"] = type;
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    return pimpl_->repository_->commitMessage(Json::writeString(wbuilder, json));
+}
+
+std::vector<std::map<std::string, std::string>>
+Conversation::loadMessages(const std::string& fromMessage, size_t n)
+{
+    return pimpl_->loadMessages(fromMessage, "", n);
+}
+
+std::vector<std::map<std::string, std::string>>
+Conversation::loadMessages(const std::string& fromMessage, const std::string& toMessage)
+{
+    return pimpl_->loadMessages(fromMessage, toMessage, 0);
+}
+
+bool
+Conversation::fetchFrom(const std::string& uri)
+{
+    // TODO check if device id or account id
+    return pimpl_->repository_->fetch(uri);
+}
+
+bool
+Conversation::mergeHistory(const std::string& uri)
+{
+    auto remoteHead = pimpl_->repository_->remoteHead(uri);
+    if (remoteHead.empty()) {
+        JAMI_WARN("Could not get HEAD of %s", uri.c_str());
+        return false;
+    }
+
+    // In the future, the diff should be analyzed to know if the
+    // history presented by the peer is correct or not.
+
+    if (!pimpl_->repository_->merge(remoteHead)) {
+        JAMI_ERR("Could not merge history with %s", uri.c_str());
+        return false;
+    }
+    JAMI_DBG("Successfully merge history with %s", uri.c_str());
+    return true;
+}
+
+} // namespace jami
\ No newline at end of file
diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h
new file mode 100644
index 0000000000000000000000000000000000000000..075415e4d5bec2e4c24f0c4b502c2683911df99c
--- /dev/null
+++ b/src/jamidht/conversation.h
@@ -0,0 +1,115 @@
+/*
+ *  Copyright (C) 2014-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <string>
+#include <vector>
+#include <map>
+#include <memory>
+
+namespace jami {
+
+class JamiAccount;
+class ConversationRepository;
+
+class Conversation
+{
+public:
+    Conversation(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId = "");
+    Conversation(const std::weak_ptr<JamiAccount>& account,
+                 const std::string& remoteDevice,
+                 const std::string& conversationId);
+    ~Conversation();
+
+    std::string id() const;
+
+    // Member management
+    /**
+     * Add conversation member
+     * @param uri   Member to add
+     * @return Commit id or empty if fails
+     */
+    std::string addMember(const std::string& contactUri);
+    bool removeMember(const std::string& contactUri);
+    /**
+     * @return a vector of member details:
+     * {
+     *  "uri":"xxx",
+     *  "role":"member/admin",
+     *  "lastRead":"id"
+     *  ...
+     * }
+     */
+    std::vector<std::map<std::string, std::string>> getMembers();
+
+    /**
+     * Join a conversation
+     * @return commit id to send
+     */
+    std::string join();
+
+    /**
+     * Test if an URI is a member
+     * @param uri       URI to test
+     * @return true if uri is a member
+     */
+    bool isMember(const std::string& uri, bool includInvited = false);
+
+    // Message send
+    std::string sendMessage(const std::string& message,
+                            const std::string& type = "text/plain",
+                            const std::string& parent = "");
+    /**
+     * Get a range of messages
+     * @param from      The most recent message ("" = last (default))
+     * @param n         Number of messages to get (0 = no limit (default))
+     * @return the range of messages
+     */
+    std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "",
+                                                                 size_t n = 0);
+    /**
+     * Get a range of messages
+     * @param fromMessage      The most recent message ("" = last (default))
+     * @param toMessage        The oldest message ("" = last (default)), no limit
+     * @return the range of messages
+     */
+    std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "",
+                                                                 const std::string& toMessage = "");
+
+    /**
+     * Get new messages from peer
+     * @param uri       the peer
+     * @return if the operation was successful
+     */
+    bool fetchFrom(const std::string& uri);
+
+    /**
+     * Analyze if merge is possible and merge history
+     * @param uri       the peer
+     * @return if the operation was successful
+     */
+    bool mergeHistory(const std::string& uri);
+
+private:
+    class Impl;
+    std::unique_ptr<Impl> pimpl_;
+};
+
+} // namespace jami
diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp
index 1ffb887086a6e09b196f698afc2df0b29fd94163..ce0619240b5627862d9292eec80d0fd5832983bc 100644
--- a/src/jamidht/conversationrepository.cpp
+++ b/src/jamidht/conversationrepository.cpp
@@ -30,6 +30,7 @@ using random_device = dht::crypto::random_device;
 #include <fstream>
 #include <json/json.h>
 #include <regex>
+#include <exception>
 
 using namespace std::string_view_literals;
 constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv;
@@ -45,15 +46,13 @@ public:
     {
         auto shared = account.lock();
         if (!shared)
-            return;
+            throw std::logic_error("No account detected when loading conversation");
         auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID()
                     + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + id_;
         git_repository* repo = nullptr;
         // TODO share this repo with GitServer
-        if (git_repository_open(&repo, path.c_str()) != 0) {
-            JAMI_WARN("Couldn't open %s", path.c_str());
-            return;
-        }
+        if (git_repository_open(&repo, path.c_str()) != 0)
+            throw std::logic_error("Couldn't open " + path);
         repository_ = {std::move(repo), git_repository_free};
     }
     ~Impl() = default;
@@ -68,6 +67,8 @@ public:
     GitDiff diff(const std::string& idNew, const std::string& idOld) const;
     std::string diffStats(const GitDiff& diff) const;
 
+    std::vector<ConversationCommit> log(const std::string& from, const std::string& to, unsigned n);
+
     std::weak_ptr<JamiAccount> account_;
     const std::string id_;
     GitRepository repository_ {nullptr, git_repository_free};
@@ -93,6 +94,27 @@ create_empty_repository(const std::string& path)
     return {std::move(repo), git_repository_free};
 }
 
+/**
+ * Add all files to index
+ * @param repo
+ * @return if operation is successful
+ */
+bool
+git_add_all(git_repository* repo)
+{
+    // git add -A
+    git_index* index_ptr = nullptr;
+    git_strarray array = {0};
+    if (git_repository_index(&index_ptr, repo) < 0) {
+        JAMI_ERR("Could not open repository index");
+        return false;
+    }
+    GitIndex index {index_ptr, git_index_free};
+    git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
+    git_index_write(index.get());
+    return true;
+}
+
 /**
  * Adds initial files. This adds the certificate of the account in the /admins directory
  * the device's key in /devices and the CRLs in /CRLs.
@@ -172,19 +194,10 @@ add_initial_files(GitRepository& repo, const std::shared_ptr<JamiAccount>& accou
         file.close();
     }
 
-    // git add -A
-    git_index* index_ptr = nullptr;
-    git_strarray array = {0};
-
-    if (git_repository_index(&index_ptr, repo.get()) < 0) {
-        JAMI_ERR("Could not open repository index");
+    if (!git_add_all(repo.get())) {
         return false;
     }
 
-    GitIndex index {index_ptr, git_index_free};
-    git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
-    git_index_write(index.get());
-
     JAMI_INFO("Initial files added in %s", repoPath.c_str());
     return true;
 }
@@ -529,7 +542,27 @@ ConversationRepository::Impl::commit(const std::string& msg)
     }
     GitSignature sig {sig_ptr, git_signature_free};
 
-    // Retrieve current HEAD
+    // Retrieve current index
+    git_index* index_ptr = nullptr;
+    if (git_repository_index(&index_ptr, repository_.get()) < 0) {
+        JAMI_ERR("Could not open repository index");
+        return {};
+    }
+    GitIndex index {index_ptr, git_index_free};
+
+    git_oid tree_id;
+    if (git_index_write_tree(&tree_id, index.get()) < 0) {
+        JAMI_ERR("Unable to write initial tree from index");
+        return {};
+    }
+
+    git_tree* tree_ptr = nullptr;
+    if (git_tree_lookup(&tree_ptr, repository_.get(), &tree_id) < 0) {
+        JAMI_ERR("Could not look up initial tree");
+        return {};
+    }
+    GitTree tree = {tree_ptr, git_tree_free};
+
     git_oid commit_id;
     if (git_reference_name_to_id(&commit_id, repository_.get(), "HEAD") < 0) {
         JAMI_ERR("Cannot get reference for HEAD");
@@ -543,13 +576,6 @@ ConversationRepository::Impl::commit(const std::string& msg)
     }
     GitCommit head_commit {head_ptr, git_commit_free};
 
-    git_tree* tree_ptr = nullptr;
-    if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) {
-        JAMI_ERR("Could not look up initial tree");
-        return {};
-    }
-    GitTree tree {tree_ptr, git_tree_free};
-
     git_buf to_sign = {};
     const git_commit* head_ref[1] = {head_commit.get()};
     if (git_commit_create_buffer(&to_sign,
@@ -605,7 +631,6 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string&
     git_oid oid;
     git_commit* commitNew = nullptr;
     if (idNew == "HEAD") {
-        JAMI_ERR("@@@ HEAD");
         if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) {
             JAMI_ERR("Cannot get reference for HEAD");
             return {nullptr, git_diff_free};
@@ -664,6 +689,89 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string&
     return {diff_ptr, git_diff_free};
 }
 
+std::vector<ConversationCommit>
+ConversationRepository::Impl::log(const std::string& from, const std::string& to, unsigned n)
+{
+    std::vector<ConversationCommit> commits {};
+
+    git_oid oid;
+    if (from.empty()) {
+        if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) {
+            JAMI_ERR("Cannot get reference for HEAD");
+            return commits;
+        }
+    } else {
+        if (git_oid_fromstr(&oid, from.c_str()) < 0) {
+            JAMI_ERR("Cannot get reference for commit %s", from.c_str());
+            return commits;
+        }
+    }
+
+    git_revwalk* walker_ptr = nullptr;
+    if (git_revwalk_new(&walker_ptr, repository_.get()) < 0
+        || git_revwalk_push(walker_ptr, &oid) < 0) {
+        JAMI_ERR("Couldn't init revwalker for conversation %s", id_.c_str());
+        return commits;
+    }
+    GitRevWalker walker {walker_ptr, git_revwalk_free};
+    git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
+
+    auto x = git_oid_tostr_s(&oid);
+    for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) {
+        if (n != 0 && idx == n) {
+            break;
+        }
+        git_commit* commit_ptr = nullptr;
+        std::string id = git_oid_tostr_s(&oid);
+        if (git_commit_lookup(&commit_ptr, repository_.get(), &oid) < 0) {
+            JAMI_WARN("Failed to look up commit %s", id.c_str());
+            break;
+        }
+        GitCommit commit {commit_ptr, git_commit_free};
+        if (id == to) {
+            break;
+        }
+
+        const git_signature* sig = git_commit_author(commit.get());
+        GitAuthor author;
+        author.name = sig->name;
+        author.email = sig->email;
+        std::vector<std::string> parents;
+        auto parentsCount = git_commit_parentcount(commit.get());
+        for (unsigned int p = 0; p < parentsCount; ++p) {
+            std::string parent {};
+            const git_oid* pid = git_commit_parent_id(commit.get(), p);
+            if (pid) {
+                parent = git_oid_tostr_s(pid);
+                parents.emplace_back(parent);
+            }
+        }
+
+        auto cc = commits.emplace(commits.end(), ConversationCommit {});
+        cc->id = std::move(id);
+        cc->commit_msg = git_commit_message(commit.get());
+        cc->author = std::move(author);
+        cc->parents = std::move(parents);
+        git_buf signature = {}, signed_data = {};
+        if (git_commit_extract_signature(&signature,
+                                         &signed_data,
+                                         repository_.get(),
+                                         &oid,
+                                         "signature")
+            < 0) {
+            JAMI_WARN("Could not extract signature for commit %s", id.c_str());
+        } else {
+            cc->signature = base64::decode(
+                std::string(signature.ptr, signature.ptr + signature.size));
+            cc->signed_content = std::vector<uint8_t>(signed_data.ptr,
+                                                      signed_data.ptr + signed_data.size);
+        }
+        cc->timestamp = git_commit_time(commit.get());
+    }
+
+    return commits;
+}
+
 std::string
 ConversationRepository::Impl::diffStats(const GitDiff& diff) const
 {
@@ -779,6 +887,49 @@ ConversationRepository::id() const
     return pimpl_->id_;
 }
 
+std::string
+ConversationRepository::addMember(const std::string& uri)
+{
+    auto account = pimpl_->account_.lock();
+    if (!account)
+        return {};
+    auto deviceId = account->currentDeviceId();
+    auto name = account->getUsername();
+    if (name.empty())
+        name = deviceId;
+
+    // First, we need to add the member file to the repository if not present
+    std::string repoPath = git_repository_workdir(pimpl_->repository_.get());
+
+    std::string invitedPath = repoPath + "invited";
+    if (!fileutils::recursive_mkdir(invitedPath, 0700)) {
+        JAMI_ERR("Error when creating %s.", invitedPath.c_str());
+        return {};
+    }
+    std::string devicePath = invitedPath + DIR_SEPARATOR_STR + uri;
+    if (fileutils::isFile(devicePath)) {
+        JAMI_WARN("Member %s already present!", uri.c_str());
+        return {};
+    }
+
+    auto file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary);
+    if (!file.is_open()) {
+        JAMI_ERR("Could not write data to %s", devicePath.c_str());
+        return {};
+    }
+    std::string path = "invited/" + uri;
+    if (!pimpl_->add(path.c_str()))
+        return {};
+    Json::Value json;
+    json["action"] = "add";
+    json["uri"] = uri;
+    json["type"] = "member";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    return pimpl_->commit(Json::writeString(wbuilder, json));
+}
+
 bool
 ConversationRepository::fetch(const std::string& remoteDeviceId)
 {
@@ -846,20 +997,17 @@ ConversationRepository::remoteHead(const std::string& remoteDeviceId, const std:
 }
 
 std::string
-ConversationRepository::sendMessage(const std::string& msg)
+ConversationRepository::commitMessage(const std::string& msg)
 {
     auto account = pimpl_->account_.lock();
     if (!account)
         return {};
     auto deviceId = std::string(account->currentDeviceId());
-    auto name = account->getDisplayName();
-    if (name.empty())
-        name = deviceId;
 
     // First, we need to add device file to the repository if not present
     std::string repoPath = git_repository_workdir(pimpl_->repository_.get());
-    std::string devicePath = repoPath + DIR_SEPARATOR_STR + "devices" + DIR_SEPARATOR_STR + deviceId
-                             + ".crt";
+    std::string path = std::string("devices") + DIR_SEPARATOR_STR + deviceId + ".crt";
+    std::string devicePath = repoPath + path;
     if (!fileutils::isFile(devicePath)) {
         auto file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary);
         if (!file.is_open()) {
@@ -871,170 +1019,23 @@ ConversationRepository::sendMessage(const std::string& msg)
         file << deviceCert;
         file.close();
 
-        // git add
-        git_index* index_ptr = nullptr;
-        if (git_repository_index(&index_ptr, pimpl_->repository_.get()) < 0) {
-            JAMI_ERR("Could not open repository index");
-            return {};
-        }
-        GitIndex index {index_ptr, git_index_free};
-
-        git_index_add_bypath(index.get(), devicePath.c_str());
-        git_index_write(index.get());
+        if (!pimpl_->add(path))
+            JAMI_WARN("Couldn't add file %s", devicePath.c_str());
     }
 
-    git_signature* sig_ptr = nullptr;
-    // Sign commit's buffer
-    if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
-        JAMI_ERR("Unable to create a commit signature.");
-        return {};
-    }
-    GitSignature sig {sig_ptr, git_signature_free};
-
-    // Retrieve current HEAD
-    git_oid commit_id;
-    if (git_reference_name_to_id(&commit_id, pimpl_->repository_.get(), "HEAD") < 0) {
-        JAMI_ERR("Cannot get reference for HEAD");
-        return {};
-    }
-
-    git_commit* head_ptr = nullptr;
-    if (git_commit_lookup(&head_ptr, pimpl_->repository_.get(), &commit_id) < 0) {
-        JAMI_ERR("Could not look up HEAD commit");
-        return {};
-    }
-    GitCommit head_commit {head_ptr, git_commit_free};
-
-    git_tree* tree_ptr = nullptr;
-    if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) {
-        JAMI_ERR("Could not look up initial tree");
-        return {};
-    }
-    GitTree tree {tree_ptr, git_tree_free};
-
-    git_buf to_sign = {};
-    const git_commit* head_ref[1] = {head_commit.get()};
-    if (git_commit_create_buffer(&to_sign,
-                                 pimpl_->repository_.get(),
-                                 sig.get(),
-                                 sig.get(),
-                                 nullptr,
-                                 msg.c_str(),
-                                 tree.get(),
-                                 1,
-                                 &head_ref[0])
-        < 0) {
-        JAMI_ERR("Could not create commit buffer");
-        return {};
-    }
-
-    // git commit -S
-    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
-    auto signed_buf = account->identity().first->sign(to_sign_vec);
-    std::string signed_str = base64::encode(signed_buf);
-    if (git_commit_create_with_signature(&commit_id,
-                                         pimpl_->repository_.get(),
-                                         to_sign.ptr,
-                                         signed_str.c_str(),
-                                         "signature")
-        < 0) {
-        JAMI_ERR("Could not sign commit");
-        return {};
-    }
-
-    // Move commit to master branch
-    git_reference* ref_ptr = nullptr;
-    if (git_reference_create(&ref_ptr,
-                             pimpl_->repository_.get(),
-                             "refs/heads/master",
-                             &commit_id,
-                             true,
-                             nullptr)
-        < 0) {
-        JAMI_WARN("Could not move commit to master");
-    }
-    git_reference_free(ref_ptr);
-
-    auto commit_str = git_oid_tostr_s(&commit_id);
-    if (commit_str) {
-        JAMI_INFO("New message added with id: %s", commit_str);
-    }
-    return commit_str ? commit_str : "";
+    return pimpl_->commit(msg);
 }
 
 std::vector<ConversationCommit>
-ConversationRepository::log(const std::string& last, unsigned n)
+ConversationRepository::logN(const std::string& last, unsigned n)
 {
-    std::vector<ConversationCommit> commits {};
-
-    git_oid oid;
-    if (last.empty()) {
-        if (git_reference_name_to_id(&oid, pimpl_->repository_.get(), "HEAD") < 0) {
-            JAMI_ERR("Cannot get reference for HEAD");
-            return commits;
-        }
-    } else {
-        if (git_oid_fromstr(&oid, last.c_str()) < 0) {
-            JAMI_ERR("Cannot get reference for commit %s", last.c_str());
-            return commits;
-        }
-    }
-
-    git_revwalk* walker_ptr = nullptr;
-    if (git_revwalk_new(&walker_ptr, pimpl_->repository_.get()) < 0
-        || git_revwalk_push(walker_ptr, &oid) < 0) {
-        JAMI_ERR("Couldn't init revwalker for conversation %s", pimpl_->id_.c_str());
-        return commits;
-    }
-    GitRevWalker walker {walker_ptr, git_revwalk_free};
-    git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
-
-    auto x = git_oid_tostr_s(&oid);
-    for (auto idx = 0; !git_revwalk_next(&oid, walker.get()); ++idx) {
-        if (n != 0 && idx == n) {
-            break;
-        }
-        git_commit* commit_ptr = nullptr;
-        std::string id = git_oid_tostr_s(&oid);
-        if (git_commit_lookup(&commit_ptr, pimpl_->repository_.get(), &oid) < 0) {
-            JAMI_WARN("Failed to look up commit %s", id.c_str());
-            break;
-        }
-        GitCommit commit {commit_ptr, git_commit_free};
-
-        const git_signature* sig = git_commit_author(commit.get());
-        GitAuthor author;
-        author.name = sig->name;
-        author.email = sig->email;
-        std::string parent {};
-        const git_oid* pid = git_commit_parent_id(commit.get(), 0);
-        if (pid) {
-            parent = git_oid_tostr_s(pid);
-        }
-
-        auto cc = commits.emplace(commits.end(), ConversationCommit {});
-        cc->id = std::move(id);
-        cc->commit_msg = git_commit_message(commit.get());
-        cc->author = std::move(author);
-        cc->parent = std::move(parent);
-        git_buf signature = {}, signed_data = {};
-        if (git_commit_extract_signature(&signature,
-                                         &signed_data,
-                                         pimpl_->repository_.get(),
-                                         &oid,
-                                         "signature")
-            < 0) {
-            JAMI_WARN("Could not extract signature for commit %s", id.c_str());
-        } else {
-            cc->signature = base64::decode(
-                std::string(signature.ptr, signature.ptr + signature.size));
-            cc->signed_content = std::vector<uint8_t>(signed_data.ptr,
-                                                      signed_data.ptr + signed_data.size);
-        }
-        cc->timestamp = git_commit_time(commit.get());
-    }
+    return pimpl_->log(last, "", n);
+}
 
-    return commits;
+std::vector<ConversationCommit>
+ConversationRepository::log(const std::string& from, const std::string& to)
+{
+    return pimpl_->log(from, to, 0);
 }
 
 bool
@@ -1153,4 +1154,57 @@ ConversationRepository::changedFiles(const std::string_view& diffStats)
     return changedFiles;
 }
 
+std::string
+ConversationRepository::join()
+{
+    if (!pimpl_)
+        return {};
+    // Check that not already member
+    std::string repoPath = git_repository_workdir(pimpl_->repository_.get());
+    auto account = pimpl_->account_.lock();
+    if (!account)
+        return {};
+    auto cert = account->identity().second;
+    auto parentCert = cert->issuer;
+    if (!parentCert) {
+        JAMI_ERR("Parent cert is null!");
+        return {};
+    }
+    auto uri = parentCert->getId().toString();
+    std::string membersPath = repoPath + "members" + DIR_SEPARATOR_STR + uri + ".crt";
+    std::string memberFile = membersPath + DIR_SEPARATOR_STR + uri + ".crt";
+    std::string adminsPath = repoPath + "admins" + DIR_SEPARATOR_STR + uri + ".crt";
+    if (fileutils::isFile(memberFile) or fileutils::isFile(adminsPath)) {
+        // Already member, nothing to commit
+        return {};
+    }
+    // Remove invited/uri.crt
+    std::string invitedPath = repoPath + "invited";
+    fileutils::remove(fileutils::getFullPath(invitedPath, uri + ".crt"));
+    // Add members/uri.crt
+    if (!fileutils::recursive_mkdir(membersPath, 0700)) {
+        JAMI_ERR("Error when creating %s. Abort create conversations", membersPath.c_str());
+        return {};
+    }
+    auto file = fileutils::ofstream(memberFile, std::ios::trunc | std::ios::binary);
+    if (!file.is_open()) {
+        JAMI_ERR("Could not write data to %s", memberFile.c_str());
+        return {};
+    }
+    file << parentCert->toString(true);
+    file.close();
+    // git add -A
+    if (!git_add_all(pimpl_->repository_.get())) {
+        return {};
+    }
+    Json::Value json;
+    json["action"] = "join";
+    json["uri"] = uri;
+    json["type"] = "member";
+    Json::StreamWriterBuilder wbuilder;
+    wbuilder["commentStyle"] = "None";
+    wbuilder["indentation"] = "";
+    return pimpl_->commit(Json::writeString(wbuilder, json));
+}
+
 } // namespace jami
diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h
index afcd6a9b2746226dfa432a7e14c673d96294acd9..c8aa376c0efd9691dd4d11422c2bef9154032e01 100644
--- a/src/jamidht/conversationrepository.h
+++ b/src/jamidht/conversationrepository.h
@@ -19,9 +19,9 @@
 
 #include <git2.h>
 #include <memory>
+#include <opendht/default_types.h>
 #include <string>
 #include <vector>
-#include <git2.h>
 
 #include "def.h"
 
@@ -56,7 +56,7 @@ struct GitAuthor
 struct ConversationCommit
 {
     std::string id {};
-    std::string parent {};
+    std::vector<std::string> parents {};
     GitAuthor author {};
     std::vector<uint8_t> signed_content {};
     std::vector<uint8_t> signature {};
@@ -99,6 +99,13 @@ public:
     ConversationRepository(const std::weak_ptr<JamiAccount>& account, const std::string& id);
     ~ConversationRepository();
 
+    /**
+     * Write the certificate in /members and commit the change
+     * @param uri    Member to add
+     * @return the commit id if successful
+     */
+    std::string addMember(const std::string& uri);
+
     /**
      * Fetch a remote repository via the given socket
      * @note This will use the socket registered for the conversation with JamiAccount::addGitSocket()
@@ -123,10 +130,10 @@ public:
 
     /**
      * Add a new commit to the conversation
-     * @param msg     The msg to send
+     * @param msg     The commit message of the commit
      * @return <empty> on failure, else the message id
      */
-    std::string sendMessage(const std::string& msg);
+    std::string commitMessage(const std::string& msg);
 
     /**
      * Get commits from [last-n, last]
@@ -134,7 +141,8 @@ public:
      * @param n     Max commits number to get (default: 0)
      * @return a list of commits
      */
-    std::vector<ConversationCommit> log(const std::string& last = "", unsigned n = 0);
+    std::vector<ConversationCommit> logN(const std::string& last = "", unsigned n = 0);
+    std::vector<ConversationCommit> log(const std::string& from = "", const std::string& to = "");
 
     /**
      * Merge another branch into the main branch
@@ -159,6 +167,12 @@ public:
      */
     static std::vector<std::string> changedFiles(const std::string_view& diffStats);
 
+    /**
+     * Join a repository
+     * @return commit Id
+     */
+    std::string join();
+
 private:
     ConversationRepository() = delete;
     class Impl;
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index c6c30568469e6394911cc0fbc51934a81f516b32..65ce0082bda5542cb4e5a3b7e3570983f457857b 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -74,6 +74,7 @@
 #include "array_size.h"
 #include "archiver.h"
 #include "data_transfer.h"
+#include "conversation.h"
 
 #include "config/yamlparser.h"
 #include "security/certstore.h"
@@ -192,6 +193,12 @@ struct JamiAccount::PendingCall
     std::shared_ptr<dht::crypto::Certificate> from_cert;
 };
 
+struct JamiAccount::PendingConversationFetch
+{
+    bool ready {false};
+    std::string deviceId {};
+};
+
 struct JamiAccount::PendingMessage
 {
     std::set<dht::InfoHash> to;
@@ -334,6 +341,13 @@ JamiAccount::~JamiAccount()
 void
 JamiAccount::shutdownConnections()
 {
+    {
+        std::lock_guard<std::mutex> lk(gitServersMtx_);
+        for (auto& [_id, gs] : gitServers_) {
+            gs->stop();
+        }
+        gitServers_.clear();
+    }
     connectionManager_.reset();
     dhtPeerConnector_.reset();
     std::lock_guard<std::mutex> lk(sipConnsMtx_);
@@ -1873,6 +1887,21 @@ JamiAccount::doRegister()
 
     JAMI_DBG("[Account %s] Starting account..", getAccountID().c_str());
 
+    JAMI_INFO("[Account %s] Start loading conversations…", getAccountID().c_str());
+    auto conversationsRepositories = fileutils::readDirectory(idPath_ + DIR_SEPARATOR_STR
+                                                              + "conversations");
+    for (const auto& repository : conversationsRepositories) {
+        try {
+            conversations_[repository] = std::move(
+                std::make_unique<Conversation>(weak(), repository));
+        } catch (const std::logic_error& e) {
+            JAMI_WARN("[Account %s] Conversations not loaded : %s",
+                      getAccountID().c_str(),
+                      e.what());
+        }
+    }
+    JAMI_INFO("[Account %s] Conversations loaded!", getAccountID().c_str());
+
     // invalid state transitions:
     // INITIALIZING: generating/loading certificates, can't register
     // NEED_MIGRATION: old account detected, user needs to migrate
@@ -2261,7 +2290,10 @@ JamiAccount::doRegister_()
         connectionManager_->onChannelRequest([this](const DeviceId&, const std::string& name) {
             auto isFile = name.substr(0, 7) == "file://";
             auto isVCard = name.substr(0, 8) == "vcard://";
-            if (name == "sip") {
+            if (name.find("git://") == 0) {
+                // TODO
+                return true;
+            } else if (name == "sip") {
                 return true;
             } else if (isFile or isVCard) {
                 auto tid_str = isFile ? name.substr(7) : name.substr(8);
@@ -2311,6 +2343,40 @@ JamiAccount::doRegister_()
                                                             tid,
                                                             std::move(channel),
                                                             std::move(cb));
+                } else if (name.find("git://") == 0) {
+                    auto conversationId = name.substr(name.find_last_of("/") + 1);
+                    {
+                        std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+                        if (pendingConversationsFetch_.find(conversationId)
+                            != pendingConversationsFetch_.end()) {
+                            // Currently cloning, so we can't offer a server.
+                            return;
+                        }
+                    }
+                    if (conversations_.find(conversationId) == conversations_.end()) {
+                        JAMI_WARN("Git server requested, but for a non existing conversation (%s)",
+                                  conversationId.c_str());
+                        return;
+                    }
+                    if (gitSocket(deviceId.toString(), conversationId) == channel) {
+                        // The onConnectionReady is already used as client (for retrieving messages)
+                        // So it's not the server socket
+                        return;
+                    }
+                    auto accountId = this->accountID_;
+                    auto gs = std::make_unique<GitServer>(accountId, conversationId, channel);
+                    const dht::Value::Id serverId = ValueIdDist()(rand);
+                    {
+                        std::lock_guard<std::mutex> lk(gitServersMtx_);
+                        gitServers_[serverId] = std::move(gs);
+                    }
+                    channel->onShutdown([w = weak(), serverId]() {
+                        auto shared = w.lock();
+                        if (!shared)
+                            return;
+                        std::lock_guard<std::mutex> lk(shared->gitServersMtx_);
+                        shared->gitServers_.erase(serverId);
+                    });
                 }
             }
         });
@@ -2375,7 +2441,24 @@ JamiAccount::doRegister_()
         if (!dhtPeerConnector_)
             dhtPeerConnector_ = std::make_unique<DhtPeerConnector>(*this);
 
-        std::lock_guard<std::mutex> bLock(buddyInfoMtx);
+        dht_->listen<ConversationRequest>(
+            inboxDeviceKey, [this, inboxDeviceKey](ConversationRequest&& req) {
+                // TODO it's a trust request, we need to confirm incoming device
+                JAMI_INFO("Receive a new conversation request for conversation %s",
+                          req.conversationId.c_str());
+                auto convId = req.conversationId;
+                std::map<std::string, std::string> metadatas = req.metadatas;
+                {
+                    std::lock_guard<std::mutex> lk(conversationsRequestsMtx_);
+                    conversationsRequests_[convId] = std::move(req);
+                }
+                emitSignal<DRing::ConversationSignal::ConversationRequestReceived>(accountID_,
+                                                                                   convId,
+                                                                                   metadatas);
+                return true;
+            });
+
+        std::lock_guard<std::mutex> lock(buddyInfoMtx);
         for (auto& buddy : trackedBuddies_) {
             buddy.second.devices_cnt = 0;
             trackPresence(buddy.first, buddy.second);
@@ -3208,6 +3291,11 @@ JamiAccount::sendTextMessage(const std::string& to,
             if (devices.find(dev) != devices.end()) {
                 return;
             }
+            // TODO do not use getAccountDetails(), accountInfo
+            if (dev.toString()
+                == getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID]) {
+                return;
+            }
 
             // Else, ask for a channel and send a DHT message
             requestSIPConnection(to, dev);
@@ -3524,6 +3612,339 @@ JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
     }
 }
 
+std::string
+JamiAccount::startConversation()
+{
+    // Create the conversation object
+    auto conversation = std::make_unique<Conversation>(weak());
+    auto convId = conversation->id();
+    conversations_[convId] = std::move(conversation);
+
+    // TODO
+    // And send an invite to others devices to sync the conversation between device
+    // Via getMemebers
+    return convId;
+}
+
+void
+JamiAccount::acceptConversationRequest(const std::string& conversationId)
+{
+    // TODO DRT to optimize connections
+    // For all conversation members, try to open a git channel with this conversation ID
+    std::unique_lock<std::mutex> lk(conversationsRequestsMtx_);
+    auto request = conversationsRequests_.find(conversationId);
+    if (request == conversationsRequests_.end()) {
+        JAMI_WARN("Request not found for conversation %s", conversationId.c_str());
+        return;
+    }
+    {
+        std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+        pendingConversationsFetch_[request->first] = PendingConversationFetch {};
+    }
+    for (const auto& member : request->second.members) {
+        auto memberHash = dht::InfoHash(member);
+        if (!memberHash) {
+            // TODO check why some members are 000000
+            continue;
+        }
+        // Avoid to connect to self for now
+        if (username_.find(member) != std::string::npos)
+            continue;
+        // TODO cf sync between devices
+        forEachDevice(memberHash, [this, request = request->second](const dht::InfoHash& dev) {
+            connectionManager().connectDevice(
+                dev,
+                "git://" + dev.toString() + "/" + request.conversationId,
+                [this, dev, request](std::shared_ptr<ChannelSocket> socket, const DeviceId&) {
+                    if (socket) {
+                        std::unique_lock<std::mutex> lk(pendingConversationsFetchMtx_);
+                        auto& pending = pendingConversationsFetch_[request.conversationId];
+                        if (!pending.ready) {
+                            pending.ready = true;
+                            pending.deviceId = dev.toString();
+                            lk.unlock();
+                            // Save the git socket
+                            addGitSocket(dev.toString(), request.conversationId, socket);
+                            // TODO when do we remove the gitSocket?
+                        } else {
+                            lk.unlock();
+                            socket->shutdown();
+                        }
+                    }
+                });
+        });
+    }
+    conversationsRequests_.erase(conversationId);
+    lk.unlock();
+    checkConversationsEvents();
+}
+
+void
+JamiAccount::checkConversationsEvents()
+{
+    bool hasHandler = conversationsEventHandler and not conversationsEventHandler->isCancelled();
+    std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+    if (not pendingConversationsFetch_.empty() and not hasHandler) {
+        conversationsEventHandler = Manager::instance().scheduler().scheduleAtFixedRate(
+            [w = weak()] {
+                if (auto this_ = w.lock())
+                    return this_->handlePendingConversations();
+                return false;
+            },
+            std::chrono::milliseconds(10));
+    } else if (pendingConversationsFetch_.empty() and hasHandler) {
+        conversationsEventHandler->cancel();
+        conversationsEventHandler.reset();
+    }
+}
+
+bool
+JamiAccount::handlePendingConversations()
+{
+    std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+    for (auto it = pendingConversationsFetch_.begin(); it != pendingConversationsFetch_.end();) {
+        if (it->second.ready) {
+            // Clone and store conversation
+            try {
+                auto conversationId = it->first;
+                auto conversation = std::make_unique<Conversation>(weak(),
+                                                                   it->second.deviceId,
+                                                                   conversationId);
+                if (conversation) {
+                    conversations_.emplace(conversationId, std::move(conversation));
+                    // Inform user that the conversation is ready
+                    emitSignal<DRing::ConversationSignal::ConversationReady>(accountID_,
+                                                                             conversationId);
+                }
+            } catch (const std::exception& e) {
+                JAMI_WARN("Something went wrong when cloning conversation: %s", e.what());
+            }
+            it = pendingConversationsFetch_.erase(it);
+        } else {
+            ++it;
+        }
+    }
+    return !pendingConversationsFetch_.empty();
+}
+
+void
+JamiAccount::declineConversationRequest(const std::string& conversationId)
+{}
+
+bool
+JamiAccount::removeConversation(const std::string& conversationId)
+{
+    return true;
+}
+
+std::vector<std::string>
+JamiAccount::getConversations()
+{
+    return {}; // TODO
+}
+
+std::vector<std::map<std::string, std::string>>
+getConversationRequests()
+{
+    return {}; // TODO
+}
+
+// Member management
+bool
+JamiAccount::addConversationMember(const std::string& conversationId, const std::string& contactUri)
+{
+    // Add a new member in the conversation
+    if (conversations_[conversationId]->addMember(contactUri).empty()) {
+        JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str());
+        return false;
+    }
+    // Invite the new member to the conversation
+    auto toH = dht::InfoHash(contactUri);
+    ConversationRequest req;
+    req.conversationId = conversationId;
+    auto convMembers = conversations_[conversationId]->getMembers();
+    for (const auto& member : convMembers)
+        req.members.emplace_back(member.at("uri"));
+    req.metadatas = {/* TODO */};
+    // TODO message engine
+    forEachDevice(toH, [this, toH, req](const dht::InfoHash& dev) {
+        JAMI_INFO("Sending conversation invite %s / %s",
+                  toH.toString().c_str(),
+                  dev.toString().c_str());
+        dht_->putEncrypted(dht::InfoHash::get("inbox:" + dev.toString()), dev, req);
+    });
+    return true;
+}
+
+bool
+JamiAccount::removeConversationMember(const std::string& conversationId,
+                                      const std::string& contactUri)
+{
+    conversations_[conversationId]->removeMember(contactUri);
+    return true;
+}
+
+std::vector<std::map<std::string, std::string>>
+JamiAccount::getConversationMembers(const std::string& conversationId)
+{
+    auto conversation = conversations_.find(conversationId);
+    if (conversation != conversations_.end() && conversation->second)
+        return conversation->second->getMembers();
+    return {};
+}
+
+// Message send/load
+void
+JamiAccount::sendMessage(const std::string& conversationId,
+                         const std::string& message,
+                         const std::string& parent,
+                         const std::string& type)
+{
+    auto conversation = conversations_.find(conversationId);
+    if (conversation != conversations_.end() && conversation->second) {
+        auto commitId = conversation->second->sendMessage(message, type, parent);
+        // TODO make async/non blocking
+        auto messages = conversation->second->loadMessages(commitId, 1);
+        if (!messages.empty()) {
+            emitSignal<DRing::ConversationSignal::MessageReceived>(getAccountID(),
+                                                                   conversationId,
+                                                                   messages.front());
+        }
+        if (!commitId.empty()) {
+            Json::Value message;
+            message["id"] = conversationId;
+            message["commit"] = commitId;
+            message["deviceId"] = getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID];
+            Json::StreamWriterBuilder builder;
+            const auto text = Json::writeString(builder, message);
+            for (const auto& members : conversation->second->getMembers()) {
+                auto uri = members.at("uri");
+                if (username_.find(uri) != std::string::npos)
+                    continue;
+                // Announce to all members that a new message is sent
+                sendTextMessage(uri, {{"application/im-gitmessage-id", text}});
+            }
+        } else {
+            JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str());
+        }
+    }
+}
+
+void
+JamiAccount::loadConversationMessages(const std::string& conversationId,
+                                      const std::string& fromMessage,
+                                      size_t n)
+{
+    // loadMessages will perform a git log that can take quite some time, so to avoid any lock, run
+    // it the threadpool
+    dht::ThreadPool::io().run([this, conversationId, fromMessage, n] {
+        auto conversation = conversations_.find(conversationId);
+        if (conversation != conversations_.end() && conversation->second) {
+            auto messages = conversation->second->loadMessages(fromMessage, n);
+            emitSignal<DRing::ConversationSignal::ConversationLoaded>(accountID_,
+                                                                      conversationId,
+                                                                      messages);
+        }
+    });
+}
+
+void
+JamiAccount::onNewGitCommit(const std::string& peer,
+                            const std::string& deviceId,
+                            const std::string& conversationId,
+                            const std::string& commitId)
+{
+    JAMI_DBG("on new commit notification from %s, for %s, commit %s",
+             peer.c_str(),
+             conversationId.c_str(),
+             commitId.c_str());
+    auto conversation = conversations_.find(conversationId);
+    if (conversation != conversations_.end() && conversation->second) {
+        if (!conversation->second->isMember(peer)) {
+            JAMI_WARN("%s is not a member of %s", peer.c_str(), conversationId.c_str());
+            return;
+        }
+
+        // Retrieve current last message
+        std::string lastMessageId = "";
+        auto lastMessage = conversation->second->loadMessages("", 1);
+        if (lastMessage.empty())
+            JAMI_ERR("No message detected. This is a bug");
+        else
+            lastMessageId = lastMessage.front().at("id");
+
+        auto announceMessages = [w = weak(), conversationId, lastMessageId]() {
+            auto shared = w.lock();
+            if (!shared)
+                return;
+            auto conversation = shared->conversations_.find(conversationId);
+            if (conversation != shared->conversations_.end() && conversation->second) {
+                // Do a diff between last message, and current new message when merged
+                auto messages = conversation->second->loadMessages("", lastMessageId);
+                for (const auto& message : messages) {
+                    JAMI_DBG("New message received for conversation %s with id %s",
+                             conversationId.c_str(),
+                             message.at("id").c_str());
+                    emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(),
+                                                                           conversationId,
+                                                                           message);
+                }
+            } else {
+                JAMI_WARN("Unknown conversation %s", conversationId.c_str());
+            }
+        };
+
+        if (gitSocket(deviceId, conversationId)) {
+            // If the git socket exists, we can fetch from it
+            if (!conversation->second->fetchFrom(deviceId)) {
+                JAMI_WARN("Could not fetch new commit from %s for %s",
+                          deviceId.c_str(),
+                          conversationId.c_str());
+                removeGitSocket(deviceId, conversationId);
+            }
+            auto merged = conversation->second->mergeHistory(deviceId);
+            if (merged)
+                announceMessages();
+        } else {
+            // Else we need to add a new gitSocket
+            {
+                std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+                pendingConversationsFetch_[conversationId] = PendingConversationFetch {};
+            }
+            connectionManager().connectDevice(
+                DeviceId(deviceId),
+                "git://" + deviceId + "/" + conversationId,
+                [this,
+                 deviceId,
+                 conversation,
+                 conversationId,
+                 announceMessages = std::move(
+                     announceMessages)](std::shared_ptr<ChannelSocket> socket, const DeviceId&) {
+                    if (socket) {
+                        addGitSocket(deviceId, conversationId, socket);
+                        if (!conversation->second->fetchFrom(deviceId))
+                            JAMI_WARN("Could not fetch new commit from %s for %s",
+                                      deviceId.c_str(),
+                                      conversationId.c_str());
+                        auto merged = conversation->second->mergeHistory(deviceId);
+                        if (merged)
+                            announceMessages();
+                    } else {
+                        JAMI_ERR("Couldn't open a new git channel with %s for conversation %s",
+                                 deviceId.c_str(),
+                                 conversationId.c_str());
+                    }
+                    {
+                        std::lock_guard<std::mutex> lk(pendingConversationsFetchMtx_);
+                        pendingConversationsFetch_.erase(conversationId);
+                    }
+                });
+        }
+    } else {
+        JAMI_WARN("Could not find conversation %s", conversationId.c_str());
+    }
+}
+
 void
 JamiAccount::cacheTurnServers()
 {
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 476ab63621703d60e7f9c4ef68e2c946c87ccfbe..005cb7516b08f5cd6cc0e794b663190fd04aa8e2 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -4,6 +4,7 @@
  *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
  *  Author: Simon Désaulniers <simon.desaulniers@gmail.com>
  *  Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -38,6 +39,7 @@
 #include "security/certstore.h"
 #include "scheduled_executor.h"
 #include "connectionmanager.h"
+#include "gitserver.h"
 
 #include <opendht/dhtrunner.h>
 #include <opendht/default_types.h>
@@ -80,6 +82,23 @@ class AccountManager;
 struct AccountInfo;
 class SipTransport;
 class ChanneledOutgoingTransfer;
+class Conversation;
+
+/**
+ * A ConversationRequest is a request which corresponds to a trust request, but for conversations
+ * It's signed by the sender and contains the members list, the conversationId, and the metadatas
+ * such as the conversation's vcard, etc. (TODO determine)
+ * Transmitted via the UDP DHT
+ */
+struct ConversationRequest : public dht::EncryptedValue<ConversationRequest>
+{
+    static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
+    dht::Value::Id id = dht::Value::INVALID_ID;
+    std::string conversationId;
+    std::vector<std::string> members;
+    std::map<std::string, std::string> metadatas;
+    MSGPACK_DEFINE_MAP(id, conversationId, members, metadatas)
+};
 
 using SipConnectionKey = std::pair<std::string /* accountId */, DeviceId>;
 using GitSocketList = std::map<std::string,                            /* device Id */
@@ -131,7 +150,7 @@ public:
      */
     JamiAccount(const std::string& accountID, bool presenceEnabled);
 
-    ~JamiAccount();
+    ~JamiAccount() noexcept;
 
     /**
      * Serialize internal state of this account for configuration
@@ -468,6 +487,34 @@ public:
     }
 
     std::string_view currentDeviceId() const;
+    // Conversation management
+    std::string startConversation();
+    void acceptConversationRequest(const std::string& conversationId);
+    void declineConversationRequest(const std::string& conversationId);
+    bool removeConversation(const std::string& conversationId);
+    std::vector<std::string> getConversations();
+    std::vector<std::map<std::string, std::string>> getConversationRequests();
+
+    // Member management
+    bool addConversationMember(const std::string& conversationId, const std::string& contactUri);
+    bool removeConversationMember(const std::string& conversationId, const std::string& contactUri);
+    std::vector<std::map<std::string, std::string>> getConversationMembers(
+        const std::string& conversationId);
+
+    // Message send/load
+    void sendMessage(const std::string& conversationId,
+                     const std::string& message,
+                     const std::string& parent = "",
+                     const std::string& type = "text/plain");
+    void loadConversationMessages(const std::string& conversationId,
+                                  const std::string& fromMessage = "",
+                                  size_t n = 0);
+
+    // Received a new commit notification
+    void onNewGitCommit(const std::string& peer,
+                        const std::string& deviceId,
+                        const std::string& conversationId,
+                        const std::string& commitId) override;
 
 private:
     NON_COPYABLE(JamiAccount);
@@ -479,6 +526,7 @@ private:
      * Private structures
      */
     struct PendingCall;
+    struct PendingConversationFetch;
     struct PendingMessage;
     struct BuddyInfo;
     struct DiscoveredPeer;
@@ -654,6 +702,9 @@ private:
     mutable std::mutex buddyInfoMtx;
     std::map<dht::InfoHash, BuddyInfo> trackedBuddies_;
 
+    /** Conversations */
+    std::map<std::string, std::unique_ptr<Conversation>> conversations_;
+
     mutable std::mutex dhtValuesMtx_;
     bool dhtPublicInCalls_ {true};
 
@@ -807,6 +858,19 @@ private:
      * @param deviceId      Device that will receive the profile
      */
     void sendProfile(const std::string& deviceId);
+
+    // Conversations
+    std::mutex conversationsRequestsMtx_ {};
+    std::map<std::string, ConversationRequest> conversationsRequests_ {};
+    std::mutex pendingConversationsFetchMtx_ {};
+    std::map<std::string, PendingConversationFetch> pendingConversationsFetch_;
+
+    std::mutex gitServersMtx_ {};
+    std::map<dht::Value::Id, std::unique_ptr<GitServer>> gitServers_ {};
+
+    std::shared_ptr<RepeatedTask> conversationsEventHandler {};
+    void checkConversationsEvents();
+    bool handlePendingConversations();
 };
 
 static inline std::ostream&
diff --git a/src/manager.cpp b/src/manager.cpp
index b970afcbbf547ac0e715a93c4efde94cf33d10f3..eb6a3f8d2ccefda4317089d2aa01e4c670c51e2e 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -750,7 +750,8 @@ Manager::init(const std::string& config_file)
     git_libgit2_init();
     auto res = git_transport_register("git", p2p_transport_cb, nullptr);
     if (res < 0) {
-        JAMI_ERR("Unable to initialize git transport");
+        const git_error* error = giterr_last();
+        JAMI_ERR("Unable to initialize git transport %s", error ? error->message : "(unknown)");
     }
 
 #ifndef WIN32
diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp
index 08f08e27c1452f64b2ac09d9e8f581506e7f63a7..35a8b85d0c7f32b3b1b676f6f0a13db6f0ff4151 100644
--- a/src/sip/sipaccountbase.cpp
+++ b/src/sip/sipaccountbase.cpp
@@ -46,6 +46,7 @@
 #include <type_traits>
 #include <regex>
 
+#include <json/json.h>
 #include <ctime>
 
 #include "manager.h"
@@ -56,6 +57,7 @@
 namespace jami {
 
 static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
+static constexpr const char MIME_TYPE_GIT[] {"application/im-gitmessage-id"};
 static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"};
 static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)};
 
@@ -561,6 +563,23 @@ SIPAccountBase::onTextMessage(const std::string& id,
             } catch (const std::exception& e) {
                 JAMI_WARN("Error parsing display notification: %s", e.what());
             }
+        } else if (m.first == MIME_TYPE_GIT) {
+            Json::Value json;
+            std::string err;
+            Json::CharReaderBuilder rbuilder;
+            auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+            if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) {
+                JAMI_ERR("Can't parse server response: %s", err.c_str());
+                return;
+            }
+
+            JAMI_WARN("Received indication for new commit available in conversation %s",
+                      json["id"].asString().c_str());
+
+            onNewGitCommit(from,
+                           json["deviceId"].asString(),
+                           json["id"].asString(),
+                           json["commit"].asString());
         }
     }
 
diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h
index 6fb154c6e937de2abf58a60bb16c10810f12fa8a..e263ed0702daee21882fba5acedaec061aad4328 100644
--- a/src/sip/sipaccountbase.h
+++ b/src/sip/sipaccountbase.h
@@ -129,7 +129,7 @@ public:
      */
     SIPAccountBase(const std::string& accountID);
 
-    virtual ~SIPAccountBase();
+    virtual ~SIPAccountBase() noexcept;
 
     /**
      * Create incoming SIPCall.
diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am
index 02e38e33c3e22860d6500e306a71d76bfebf8753..25b8756d2a989a3334e871ce6df1d2fb3a9aff8c 100644
--- a/test/unitTest/Makefile.am
+++ b/test/unitTest/Makefile.am
@@ -140,4 +140,10 @@ ut_fileTransfer_SOURCES = fileTransfer/fileTransfer.cpp
 check_PROGRAMS += ut_conversationRepository
 ut_conversationRepository_SOURCES = conversationRepository/conversationRepository.cpp
 
+#
+# conversation
+#
+check_PROGRAMS += ut_conversation
+ut_conversation_SOURCES = conversation/conversation.cpp
+
 TESTS = $(check_PROGRAMS)
diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b0c7e1758cdd3da17085565001d53cf323e70e2c
--- /dev/null
+++ b/test/unitTest/conversation/conversation.cpp
@@ -0,0 +1,488 @@
+/*
+ *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <cppunit/TestAssert.h>
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <condition_variable>
+#include <string>
+#include <fstream>
+#include <streambuf>
+#include <filesystem>
+
+#include "manager.h"
+#include "jamidht/conversation.h"
+#include "jamidht/jamiaccount.h"
+#include "../../test_runner.h"
+#include "dring.h"
+#include "base64.h"
+#include "fileutils.h"
+#include "account_const.h"
+
+#include <git2.h>
+
+using namespace std::string_literals;
+using namespace DRing::Account;
+
+namespace jami {
+namespace test {
+
+class ConversationTest : public CppUnit::TestFixture
+{
+public:
+    ~ConversationTest() { DRing::fini(); }
+    static std::string name() { return "Conversation"; }
+    void setUp();
+    void tearDown();
+
+    std::string aliceId;
+    std::string bobId;
+
+private:
+    void testCreateConversation();
+    void testGetConversation();
+    void testAddMember();
+    void testGetMembers();
+    void testSendMessage();
+    void testSendMessageTriggerMessageReceived();
+    void testMergeTwoDifferentHeads();
+
+    CPPUNIT_TEST_SUITE(ConversationTest);
+    CPPUNIT_TEST(testCreateConversation);
+    CPPUNIT_TEST(testGetConversation);
+    CPPUNIT_TEST(testAddMember);
+    CPPUNIT_TEST(testGetMembers);
+    CPPUNIT_TEST(testSendMessage);
+    CPPUNIT_TEST(testSendMessageTriggerMessageReceived);
+    CPPUNIT_TEST_SUITE_END();
+};
+
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationTest, ConversationTest::name());
+
+void
+ConversationTest::setUp()
+{
+    // Init daemon
+    DRing::init(DRing::InitFlag(DRing::DRING_FLAG_DEBUG | DRing::DRING_FLAG_CONSOLE_LOG));
+    if (not Manager::instance().initialized)
+        CPPUNIT_ASSERT(DRing::start("dring-sample.yml"));
+
+    std::map<std::string, std::string> details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "ALICE";
+    details[ConfProperties::ALIAS] = "ALICE";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = "";
+    aliceId = Manager::instance().addAccount(details);
+
+    details = DRing::getAccountTemplate("RING");
+    details[ConfProperties::TYPE] = "RING";
+    details[ConfProperties::DISPLAYNAME] = "BOB";
+    details[ConfProperties::ALIAS] = "BOB";
+    details[ConfProperties::UPNP_ENABLED] = "true";
+    details[ConfProperties::ARCHIVE_PASSWORD] = "";
+    details[ConfProperties::ARCHIVE_PIN] = "";
+    details[ConfProperties::ARCHIVE_PATH] = "";
+    bobId = Manager::instance().addAccount(details);
+
+    JAMI_INFO("Initialize account...");
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                bool ready = false;
+                auto details = aliceAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                ready = (daemonStatus == "REGISTERED");
+                details = bobAccount->getVolatileAccountDetails();
+                daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                ready &= (daemonStatus == "REGISTERED");
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+    cv.wait_for(lk, std::chrono::seconds(30));
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::tearDown()
+{
+    auto currentAccSize = Manager::instance().getAccountList().size();
+    Manager::instance().removeAccount(aliceId, true);
+    Manager::instance().removeAccount(bobId, true);
+    // Because cppunit is not linked with dbus, just poll if removed
+    for (int i = 0; i < 40; ++i) {
+        if (Manager::instance().getAccountList().size() <= currentAccSize - 2)
+            break;
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    }
+}
+
+void
+ConversationTest::testCreateConversation()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceDeviceId = aliceAccount
+                             ->getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID];
+    auto uri = aliceAccount->getAccountDetails()[DRing::Account::ConfProperties::USERNAME];
+    if (uri.find("ring:") == 0)
+        uri = uri.substr(std::string("ring:").size());
+    auto convId = aliceAccount->startConversation();
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Check created files
+    auto adminCrt = repoPath + DIR_SEPARATOR_STR + "admins" + DIR_SEPARATOR_STR + uri + ".crt";
+    CPPUNIT_ASSERT(fileutils::isFile(adminCrt));
+    auto crt = std::ifstream(adminCrt);
+    std::string adminCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>());
+    auto cert = aliceAccount->identity().second;
+    auto deviceCert = cert->toString(false);
+    auto parentCert = cert->issuer->toString(true);
+    CPPUNIT_ASSERT(adminCrtStr == parentCert);
+    auto deviceCrt = repoPath + DIR_SEPARATOR_STR + "devices" + DIR_SEPARATOR_STR + aliceDeviceId
+                     + ".crt";
+    CPPUNIT_ASSERT(fileutils::isFile(deviceCrt));
+    crt = std::ifstream(deviceCrt);
+    std::string deviceCrtStr((std::istreambuf_iterator<char>(crt)),
+                             std::istreambuf_iterator<char>());
+    CPPUNIT_ASSERT(deviceCrtStr == deviceCert);
+}
+
+void
+ConversationTest::testGetConversation()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto aliceDeviceId = aliceAccount
+                             ->getAccountDetails()[DRing::Account::ConfProperties::RING_DEVICE_ID];
+    auto uri = aliceAccount->getAccountDetails()[DRing::Account::ConfProperties::USERNAME];
+    if (uri.find("ring:") == 0)
+        uri = uri.substr(std::string("ring:").size());
+    auto convId = aliceAccount->startConversation();
+
+    auto conversations = aliceAccount->getConversations();
+    CPPUNIT_ASSERT(conversations.size() == 1);
+    CPPUNIT_ASSERT(conversations.front() == convId);
+}
+
+void
+ConversationTest::testAddMember()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME];
+    if (bobUri.find("ring:") == 0)
+        bobUri = bobUri.substr(std::string("ring:").size());
+    auto convId = aliceAccount->startConversation();
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, requestReceived = false, memberMessageGenerated = false;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == aliceId && conversationId == convId && message["type"] == "member") {
+                memberMessageGenerated = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    CPPUNIT_ASSERT(memberMessageGenerated);
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Check created files
+    auto bobInvited = repoPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri
+                      + ".crt";
+    CPPUNIT_ASSERT(fileutils::isFile(bobInvited));
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; }));
+    bobAccount->acceptConversationRequest(convId);
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }));
+    auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                      + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
+    bobInvited = clonedPath + DIR_SEPARATOR_STR + "invited" + DIR_SEPARATOR_STR + bobUri + ".crt";
+    CPPUNIT_ASSERT(!fileutils::isFile(bobInvited));
+}
+
+void
+ConversationTest::testGetMembers()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME];
+    auto aliceUri = aliceAccount->getAccountDetails()[ConfProperties::USERNAME];
+    if (bobUri.find("ring:") == 0)
+        bobUri = bobUri.substr(std::string("ring:").size());
+    if (aliceUri.find("ring:") == 0)
+        aliceUri = aliceUri.substr(std::string("ring:").size());
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageReceived = false;
+    bool requestReceived = false;
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == aliceId) {
+                messageReceived = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == bobId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+    // Start a conversation and add member
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+
+    auto members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 1);
+    CPPUNIT_ASSERT(members[0]["uri"]
+                   == aliceAccount->getAccountDetails()[ConfProperties::USERNAME].substr(
+                       std::string("ring:").size()));
+    CPPUNIT_ASSERT(members[0]["role"] == "admin");
+
+    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; });
+    messageReceived = false;
+    bobAccount->acceptConversationRequest(convId);
+    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; });
+    members = bobAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 2);
+    cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceived; });
+    members = aliceAccount->getConversationMembers(convId);
+    CPPUNIT_ASSERT(members.size() == 2);
+    auto hasBob = members[0]["uri"] == bobUri || members[1]["uri"] == bobUri;
+    auto hasAlice = members[0]["uri"] == aliceUri || members[1]["uri"] == aliceUri;
+    CPPUNIT_ASSERT(hasAlice && hasBob);
+}
+
+void
+ConversationTest::testSendMessage()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getAccountDetails()[ConfProperties::USERNAME];
+    if (bobUri.find("ring:") == 0)
+        bobUri = bobUri.substr(std::string("ring:").size());
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageBobReceived = 0, messageAliceReceived = 0;
+    bool requestReceived = false;
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            if (accountId == bobId) {
+                messageBobReceived += 1;
+            } else {
+                messageAliceReceived += 1;
+            }
+            cv.notify_one();
+        }));
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>(
+            [&](const std::string& /*accountId*/,
+                const std::string& /* conversationId */,
+                std::map<std::string, std::string> /*metadatas*/) {
+                requestReceived = true;
+                cv.notify_one();
+            }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& /*accountId*/, const std::string& /* conversationId */) {
+            conversationReady = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri));
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(requestReceived);
+
+    bobAccount->acceptConversationRequest(convId);
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(conversationReady);
+
+    // Assert that repository exists
+    auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID()
+                    + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId;
+    CPPUNIT_ASSERT(fileutils::isDirectory(repoPath));
+    // Wait that alice sees Bob
+    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; });
+
+    aliceAccount->sendMessage(convId, "hi");
+    cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; });
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testSendMessageTriggerMessageReceived()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    auto messageReceived = 0;
+    bool conversationReady = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& /* accountId */,
+            const std::string& /* conversationId */,
+            std::map<std::string, std::string> /*message*/) {
+            messageReceived += 1;
+            cv.notify_one();
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& /* accountId */, const std::string& /* conversationId */) {
+            conversationReady = true;
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    auto convId = aliceAccount->startConversation();
+    cv.wait_for(lk, std::chrono::seconds(30));
+    CPPUNIT_ASSERT(conversationReady);
+
+    aliceAccount->sendMessage(convId, "hi");
+    cv.wait_for(lk, std::chrono::seconds(30), [&] { return messageReceived == 1; });
+    CPPUNIT_ASSERT(messageReceived == 1);
+    DRing::unregisterSignalHandlers();
+}
+
+void
+ConversationTest::testMergeTwoDifferentHeads()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId);
+    auto carlaUri = carlaAccount->getUsername();
+    aliceAccount->trackBuddyPresence(carlaUri, true);
+    auto convId = aliceAccount->startConversation();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    bool conversationReady = false, carlaGotMessage = false;
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>(
+        [&](const std::string& accountId, const std::string& /* conversationId */) {
+            if (accountId == carlaId) {
+                conversationReady = true;
+                cv.notify_one();
+            }
+        }));
+    confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>(
+        [&](const std::string& accountId,
+            const std::string& conversationId,
+            std::map<std::string, std::string> message) {
+            if (accountId == carlaId && conversationId == convId) {
+                carlaGotMessage = true;
+            }
+            cv.notify_one();
+        }));
+    DRing::registerSignalHandlers(confHandlers);
+
+    CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false));
+
+    // Cp conversations & convInfo
+    auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR
+                         + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations";
+    std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive);
+    auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID()
+                       + DIR_SEPARATOR_STR + "convInfo";
+    std::filesystem::copy(ciPathAlice, ciPathCarla);
+
+    // Accept for alice and makes different heads
+    ConversationRepository repo(carlaAccount, convId);
+    repo.join();
+
+    aliceAccount->sendMessage(convId, "hi");
+    aliceAccount->sendMessage(convId, "sup");
+    aliceAccount->sendMessage(convId, "jami");
+
+    // Start Carla, should merge and all messages should be there
+    Manager::instance().sendRegister(carlaId, true);
+    carlaGotMessage = false;
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return carlaGotMessage; }));
+    DRing::unregisterSignalHandlers();
+}
+
+} // namespace test
+} // namespace jami
+
+RING_TEST_RUNNER(jami::test::ConversationTest::name())
diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp
index 4da580e76d3e7f8fef5845c1633a329218ed4628..f10834d0823271f7bbaa01fbe4ed2503c21c0170 100644
--- a/test/unitTest/conversationRepository/conversationRepository.cpp
+++ b/test/unitTest/conversationRepository/conversationRepository.cpp
@@ -118,23 +118,27 @@ ConversationRepositoryTest::setUp()
     details[ConfProperties::ARCHIVE_PATH] = "";
     bobId = Manager::instance().addAccount(details);
 
+    JAMI_INFO("Initialize account...");
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
     auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
-
-    bool ready = false;
-    bool idx = 0;
-    while (!ready && idx < 100) {
-        auto details = aliceAccount->getVolatileAccountDetails();
-        auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
-        ready = (daemonStatus == "REGISTERED");
-        details = bobAccount->getVolatileAccountDetails();
-        daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
-        ready &= (daemonStatus == "REGISTERED");
-        if (!ready) {
-            idx += 1;
-            std::this_thread::sleep_for(std::chrono::milliseconds(100));
-        }
-    }
+    std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers;
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    confHandlers.insert(
+        DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>(
+            [&](const std::string&, const std::map<std::string, std::string>&) {
+                bool ready = false;
+                auto details = aliceAccount->getVolatileAccountDetails();
+                auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                ready = (daemonStatus == "REGISTERED");
+                details = bobAccount->getVolatileAccountDetails();
+                daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS];
+                ready &= (daemonStatus == "REGISTERED");
+            }));
+    DRing::registerSignalHandlers(confHandlers);
+    cv.wait_for(lk, std::chrono::seconds(30));
+    DRing::unregisterSignalHandlers();
 }
 
 void
@@ -253,7 +257,7 @@ ConversationRepositoryTest::testCloneViaChannelSocket()
 
     aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId),
                                                     "git://*",
-                                                    [&](std::shared_ptr<ChannelSocket> socket) {
+                                                    [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) {
                                                         if (socket) {
                                                             successfullyConnected = true;
                                                             sendSocket = socket;
@@ -269,13 +273,11 @@ ConversationRepositoryTest::testCloneViaChannelSocket()
 
     bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket);
     GitServer gs(aliceId, repository->id(), sendSocket);
-    std::thread sendT = std::thread([&]() { gs.run(); });
 
     auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(),
                                                             aliceDeviceId,
                                                             repository->id());
     gs.stop();
-    sendT.join();
 
     CPPUNIT_ASSERT(cloned != nullptr);
     CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath));
@@ -337,19 +339,19 @@ ConversationRepositoryTest::testAddSomeMessages()
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
     auto repository = ConversationRepository::createConversation(aliceAccount->weak());
 
-    auto id1 = repository->sendMessage("Commit 1");
-    auto id2 = repository->sendMessage("Commit 2");
-    auto id3 = repository->sendMessage("Commit 3");
+    auto id1 = repository->commitMessage("Commit 1");
+    auto id2 = repository->commitMessage("Commit 2");
+    auto id3 = repository->commitMessage("Commit 3");
 
     auto messages = repository->log();
     CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */);
     CPPUNIT_ASSERT(messages[0].id == id3);
-    CPPUNIT_ASSERT(messages[0].parent == id2);
+    CPPUNIT_ASSERT(messages[0].parents.front() == id2);
     CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3");
     CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email);
     CPPUNIT_ASSERT(messages[1].id == id2);
-    CPPUNIT_ASSERT(messages[1].parent == id1);
+    CPPUNIT_ASSERT(messages[1].parents.front() == id1);
     CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2");
     CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email);
@@ -357,7 +359,7 @@ ConversationRepositoryTest::testAddSomeMessages()
     CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1");
     CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email);
-    CPPUNIT_ASSERT(messages[2].parent == repository->id());
+    CPPUNIT_ASSERT(messages[2].parents.front() == repository->id());
     // Check sig
     CPPUNIT_ASSERT(
         aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content,
@@ -376,18 +378,18 @@ ConversationRepositoryTest::testLogMessages()
     auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
     auto repository = ConversationRepository::createConversation(aliceAccount->weak());
 
-    auto id1 = repository->sendMessage("Commit 1");
-    auto id2 = repository->sendMessage("Commit 2");
-    auto id3 = repository->sendMessage("Commit 3");
+    auto id1 = repository->commitMessage("Commit 1");
+    auto id2 = repository->commitMessage("Commit 2");
+    auto id3 = repository->commitMessage("Commit 3");
 
-    auto messages = repository->log(repository->id(), 1);
+    auto messages = repository->logN(repository->id(), 1);
     CPPUNIT_ASSERT(messages.size() == 1);
     CPPUNIT_ASSERT(messages[0].id == repository->id());
-    messages = repository->log(id2, 2);
+    messages = repository->logN(id2, 2);
     CPPUNIT_ASSERT(messages.size() == 2);
     CPPUNIT_ASSERT(messages[0].id == id2);
     CPPUNIT_ASSERT(messages[1].id == id1);
-    messages = repository->log(repository->id(), 3);
+    messages = repository->logN(repository->id(), 3);
     CPPUNIT_ASSERT(messages.size() == 1);
     CPPUNIT_ASSERT(messages[0].id == repository->id());
 }
@@ -436,7 +438,7 @@ ConversationRepositoryTest::testFetch()
 
     aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId),
                                                     "git://*",
-                                                    [&](std::shared_ptr<ChannelSocket> socket) {
+                                                    [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) {
                                                         if (socket) {
                                                             successfullyConnected = true;
                                                             sendSocket = socket;
@@ -453,25 +455,23 @@ ConversationRepositoryTest::testFetch()
 
     bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket);
     GitServer gs(aliceId, repository->id(), sendSocket);
-    std::thread sendT = std::thread([&]() { gs.run(); });
 
     // Clone repository
-    auto id1 = repository->sendMessage("Commit 1");
+    auto id1 = repository->commitMessage("Commit 1");
     auto cloned = ConversationRepository::cloneConversation(bobAccount->weak(),
                                                             aliceDeviceId,
                                                             repository->id());
     gs.stop();
-    sendT.join();
     bobAccount->removeGitSocket(aliceDeviceId, repository->id());
 
     // Add some new messages to fetch
-    auto id2 = repository->sendMessage("Commit 2");
-    auto id3 = repository->sendMessage("Commit 3");
+    auto id2 = repository->commitMessage("Commit 2");
+    auto id3 = repository->commitMessage("Commit 3");
 
     // Open a new channel to simulate the fact that we are later
     aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId),
                                                     "git://*",
-                                                    [&](std::shared_ptr<ChannelSocket> socket) {
+                                                    [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) {
                                                         if (socket) {
                                                             successfullyConnected = true;
                                                             sendSocket = socket;
@@ -484,24 +484,22 @@ ConversationRepositoryTest::testFetch()
     ccv.wait_for(lk, std::chrono::seconds(10));
     bobAccount->addGitSocket(aliceDeviceId, repository->id(), channelSocket);
     GitServer gs2(aliceId, repository->id(), sendSocket);
-    std::thread sendT2 = std::thread([&]() { gs2.run(); });
 
     CPPUNIT_ASSERT(cloned->fetch(aliceDeviceId));
     CPPUNIT_ASSERT(id3 == cloned->remoteHead(aliceDeviceId));
 
     gs2.stop();
     bobAccount->removeGitSocket(aliceDeviceId, repository->id());
-    sendT2.join();
 
     auto messages = cloned->log(id3);
     CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */);
     CPPUNIT_ASSERT(messages[0].id == id3);
-    CPPUNIT_ASSERT(messages[0].parent == id2);
+    CPPUNIT_ASSERT(messages[0].parents.front() == id2);
     CPPUNIT_ASSERT(messages[0].commit_msg == "Commit 3");
     CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email);
     CPPUNIT_ASSERT(messages[1].id == id2);
-    CPPUNIT_ASSERT(messages[1].parent == id1);
+    CPPUNIT_ASSERT(messages[1].parents.front() == id1);
     CPPUNIT_ASSERT(messages[1].commit_msg == "Commit 2");
     CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email);
@@ -509,7 +507,7 @@ ConversationRepositoryTest::testFetch()
     CPPUNIT_ASSERT(messages[2].commit_msg == "Commit 1");
     CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name);
     CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email);
-    CPPUNIT_ASSERT(messages[2].parent == repository->id());
+    CPPUNIT_ASSERT(messages[2].parents.front() == repository->id());
     // Check sig
     CPPUNIT_ASSERT(
         aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content,
@@ -685,9 +683,9 @@ ConversationRepositoryTest::testDiff()
     auto uri = aliceAccount->getUsername();
     auto repository = ConversationRepository::createConversation(aliceAccount->weak());
 
-    auto id1 = repository->sendMessage("Commit 1");
-    auto id2 = repository->sendMessage("Commit 2");
-    auto id3 = repository->sendMessage("Commit 3");
+    auto id1 = repository->commitMessage("Commit 1");
+    auto id2 = repository->commitMessage("Commit 2");
+    auto id3 = repository->commitMessage("Commit 3");
 
     auto diff = repository->diffStats(id2, id1);
     CPPUNIT_ASSERT(ConversationRepository::changedFiles(diff).empty());