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. The export_domain restricts 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 the mail.message and immediately pushes a mail.message/insert notification to bus.bus so 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.message where message_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_id

Written directly from $message_id.

body

Written directly from $body.

res_id (channel mapping)

Lookup: discuss.channel where sync_channel_uuid = $channel_uuid. (Note: model is explicitly set to discuss.channel in the schema). On miss: create the channel with:

  • name ← $channel_name

  • channel_type ← "channel" (always)

  • parent_channel_id: lookup discuss.channel where sync_channel_uuid = $parent_channel_uuid; left null on miss. For root messages $channel_uuid == $parent_channel_uuid so 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: lookup mail.message where message_id = $from_message_id. On miss: create a stub mail.message anchored to the main channel via res_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.message where message_id = $parent_message_id. On miss: create a minimal stub mail.message carrying only message_id. When the parent payload arrives later, __root__ upserts the stub in-place so the reply’s FK remains valid throughout. When $parent_message_id is null, no lookup is attempted and the field is omitted.

author_id (message author)

Lookup: res.partner where peer_id.peer_char = $author_peer. On miss: field omitted — Odoo fills author_id from env.user.partner_id (API user for instant events, cron user for batch imports). On hit: image_1920 is also written from $author_avatar_cid via the is_cid: store trait, which fetches the binary payload and base64-encodes it into the binary field.

attachment_ids (file attachments)

Batch lookup over every $attachments[*].cid / $attachments[*].name pair: find ir.attachment where cid_id.cid = $cid and name = $name. The cid_id side uses the is_cid: attach trait, which resolves the raw CID string to the local ip.cid FK before the cache comparison. On miss: create a new attachment with type = "url". command: set replaces 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[*].content pair: find mail.message.reaction where partner_id.peer_id.peer_char = $author_peer and content = $content. On miss: create a new reaction record. command: set is scoped to reactions whose partner_id.peer_id.host_node_id matches 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.