Sync Discuss App¶
Sync Odoo Discuss channels, messages, and attachments across independent databases in real time.
What Gets Synced¶
Channels: Channel existence is synchronized across databases using the sync_channel_uuid.
Missing channels are automatically created on-the-fly when messages arrive (see Setup for joining details).
Messages, Edits, and Deletes: The core message body and author identity are fully synchronized. Edits and deletions (tombstones) are synchronized natively as in-place body updates. Message timestamps are not explicitly transferred; instead, they rely on the local node’s arrival and creation time to ensure chronological consistency within the receiving database.
Replies, Threads: Both regular channel messages and sub-thread replies are preserved and reconstructed dynamically.
The engine uses the sync_main_channel_id to carry the thread structure across nodes, ensuring that replies are properly nested under their parent messages.
Reactions: Reactions are not synced as discrete objects; instead, adding or removing a reaction simply re-exports the parent message so the receiving node can update its reaction count.
Attachments and Avatars: File attachments and author avatars are transferred as CID (Content Identifier) blobs via the is_cid schema trait.
The message import awaits the completion of the CID fetch; therefore, messages carrying attachments or new avatars will only appear in the Discuss UI once the background file transfer is fully complete.
Setup¶
1. Install sync_discuss¶
Install sync_discuss on both Odoo instances.
It depends on sync_base and mail; both are pulled in automatically.
Navigate to the /odoo/discuss/v1 topic and set the swarm_ids field to include the swarms you want to sync with.
This will restrict message exchange to nodes in those swarms.
2. Configure a channel¶
The sync_channel_uuid is the cross-node routing key.
It is assigned automatically when a new channel is created.
You only need to manually set it:
On pre-existing channels that were created before installing the module.
On channels that need to be mapped to an explicit UUID (e.g., if you are linking a local channel to a specific channel on a foreign database).
Next, link the channel members to peer nodes.
Each member’s res.partner must be linked to an ip.peer with a host_node_id pointing to a reachable ip.node (i.e., the node belongs to a swarm that is active on the Discuss topic).
The __nodes__ expression in the Discuss schema resolves this path:
sync_main_channel_id.channel_member_ids.partner_id.peer_id.host_node_id
Events are only created for partners hosted by foreign nodes. Without a reachable link, the routing engine cannot determine where to send the event for that member.
3. Join a channel¶
If a message arrives for a sync_channel_uuid that does not exist locally, the schema engine creates the channel automatically (on_miss: create).
Users on the foreign database will need to find the created channel and join it from the Discuss application (or an administrator can add them as members) to see it in their sidebar. Alternatively, they can configure a channel with the same UUID beforehand and join it.
Topic Reference¶
The /odoo/discuss/v1 topic orchestrates the synchronization behavior for Discuss using the following configurations:
Routing (
direct): The engine uses the__nodes__schema path to resolve exactly which remote nodes host participants of the channel, creating one targeted outbound event per remote node.Export (
instant): Automatically triggers when a user posts a message. Theexport_domainrestricts exports to channel messages with valid foreign peers:[ ('message_type', '=', 'comment'), ('sync_main_channel_id', '!=', False), ('sync_main_channel_id.channel_member_ids.partner_id.peer_id', '!=', False), ('sync_main_channel_id.channel_member_ids.partner_id.peer_id.host_node_id.is_gateway', '=', False), ]
message_type: Only matches actual user comments (not system notifications).sync_main_channel_id: Targets the root channel so sub-channels always match the domain identically.peer_id: Ensures at least one member in the channel is mapped to a network peer.is_gateway = False: Ensures at least one peer in the channel is hosted by a foreign node. If all peers belong to the local gateway, the export is skipped entirely.
Delivery (
push): The local gateway actively opens a P2P connection to push the event to each remote peer immediately, typically arriving in under a second.Import (
instant): Automatically processes the inbound event upon receipt. The schema creates or updates themail.messageand immediately pushes amail.message/insertnotification tobus.busso the Discuss UI updates live without a page reload.Retention (
auto_delete): A scheduled task garbage collects fully processed inbound and outbound events (e.g., mapped, sent, or delivered) that are older than 7 days, keeping the database footprint minimal.
Schema reference¶
The $ prefix denotes JSON payload fields (e.g., $message_id).
__root__→id(record identity)Upsert key: find
mail.messagewheremessage_id = $message_id. On miss: create a new record. On hit: update in-place — this is how stubs created by earlier out-of-order imports are healed when the real payload eventually arrives.message_idWritten directly from
$message_id.bodyWritten directly from
$body.res_id(channel mapping)Lookup:
discuss.channelwheresync_channel_uuid = $channel_uuid. (Note:modelis explicitly set todiscuss.channelin the schema). On miss: create the channel with:name←$channel_namechannel_type←"channel"(always)parent_channel_id: lookupdiscuss.channelwheresync_channel_uuid = $parent_channel_uuid; left null on miss. For root messages$channel_uuid == $parent_channel_uuidso the lookup always fails and null is correct. For thread replies the root channel is expected to exist; if it does not (two anchors missing simultaneously) the sub-channel is created as an unrooted orphan — a known limitation.from_message_id: lookupmail.messagewheremessage_id = $from_message_id. On miss: create a stubmail.messageanchored to the main channel viares_id. When the parent message payload arrives later,__root__upserts the stub in-place so the thread opener link works immediately or gracefully heals.
parent_id(reply-to threading)Lookup:
mail.messagewheremessage_id = $parent_message_id. On miss: create a minimal stubmail.messagecarrying onlymessage_id. When the parent payload arrives later,__root__upserts the stub in-place so the reply’s FK remains valid throughout. When$parent_message_idis null, no lookup is attempted and the field is omitted.author_id(message author)Lookup:
res.partnerwherepeer_id.peer_char = $author_peer. On miss: field omitted — Odoo fillsauthor_idfromenv.user.partner_id(API user for instant events, cron user for batch imports). On hit:image_1920is also written from$author_avatar_cidvia theis_cid: storetrait, which fetches the binary payload and base64-encodes it into the binary field.attachment_ids(file attachments)Batch lookup over every
$attachments[*].cid/$attachments[*].namepair: findir.attachmentwherecid_id.cid = $cidandname = $name. Thecid_idside uses theis_cid: attachtrait, which resolves the raw CID string to the localip.cidFK before the cache comparison. On miss: create a new attachment withtype = "url".command: setreplaces the entire attachment set on every import so that edits removing attachments are correctly reflected.reaction_ids(emoji reactions)Batch lookup over every
$reactions[*].author_peer/$reactions[*].contentpair: findmail.message.reactionwherepartner_id.peer_id.peer_char = $author_peerandcontent = $content. On miss: create a new reaction record.command: setis scoped to reactions whosepartner_id.peer_id.host_node_idmatches the event’s source node (__from__), so only that node’s reactions are replaced; other nodes’ reactions are left untouched.
License¶
LGPL-3
See also
sync_discuss — App — Technical Reference.