Connect by JBRH Open Connect

From a provider message to a canonical one

Mail exists twice on purpose. tenant_* tables hold what the provider adapter fetched, in the provider's own shape; the canonical threads, messages and contacts are what the engine and every screen read. connect_core.bridge_* copies between them, in one direction, and the engine never reads the provider tables at all.

Status
Available What this means
Audience
both, developer
Channels
email
In the app
#/inbox
Last verified
Product version
6.3.2

Two models, one direction#

An adapter's output is shaped by its provider: Gmail's identifiers and labels, Graph's own fields, an IMAP server's UIDs and flags. Storing that faithfully is the right thing to do — it is the evidence of what actually arrived — and building a product on it would be the wrong thing, because every screen and every decision would then carry three shapes.

So the adapter writes tenant_* and stops. connect_core.bridge_* copies into the canonical records, and everything downstream reads only those. The flow is one-way: nothing after the bridge writes back into the provider tables, and nothing before it reads the canonical ones.

Provider sideCanonical side
tenant_*, in the provider's shapethreads, messages, contacts
Written by the adapterWritten by connect_core.bridge_*
Read by the bridgeRead by the engine, every screen, the Assistant and the audit trail
Meaningful only alongside its providerProvider-independent by construction
Discarded relevance once bridgedThe workspace's actual history, outliving any connection

What the separation buys#

  1. Adding a provider changes nothing downstream. The canonical shape is the only thing anything after the bridge has ever seen, so a new adapter is an adapter rather than a migration.
  2. A bug in one adapter cannot corrupt conversation history. It can corrupt its own table, which is recoverable, rather than the workspace's record of what was said to whom.
  3. Disconnecting a mailbox does not delete a workspace's past. Canonical records were copied, not referenced — see Disconnecting a mailbox.
  4. One triage model, not three. A person's own marks — threads.priority, threads.starred, threads.deleted_at — live on the canonical rows, so they mean the same thing whatever the message arrived through.

The cost is real and worth naming: the same message is stored twice, and the bridge is a place where things can fail. That trade was made deliberately, and the failure mode it buys — a bridge that stops — is loud and local, where the one it avoids is silent and spread across every screen.

What the bridge produces#

A thread
The conversation the message belongs to, matched or created — Conversation threads.
A message
The canonical record: who, when, what, on which thread, through which mailbox.
A contact
The counterparty as a record, which is then resolved to a person and possibly a company — Resolving a sender to a person.

Everything the workspace does afterwards hangs off those three. Triage reads them, the engine grounds a draft against them, memory and knowledge resolve against the people they name, and the audit trail refers to them. None of that machinery has a provider-specific branch, and that is the property being protected.

When something does not appear#

A message that reached tenant_* but is not in Conversations has stopped at the bridge, and that is a different problem from a mailbox that fetched nothing. The distinguishing question is whether anything at all arrived in that window: nothing arriving is a quiet mailbox or a health problem; some things arriving and one not is a canonicalisation problem.

The cursor rule protects you in both cases. A position advances only for work that produced a canonical record, so a message the bridge rejected leaves the position where it was and is offered again rather than passed over — IMAP UID and cursor lifecycle sets out the outcomes.

Questions#

Why store the same message twice?

Because the two copies answer different questions. The provider copy is evidence of exactly what arrived in the provider's own terms; the canonical copy is the workspace's record of the conversation. Collapsing them would make every screen and every decision provider-aware.

Can the engine be pointed at the provider tables for speed?

That is the shortcut the design exists to refuse. The moment the engine reads a provider shape, adding a provider stops being free and a bug in one adapter stops being contained.

Does the canonical record keep the original message?

The provider-side record is what holds the message as the provider expressed it. How the body is displayed to you is a separate concern handled by mail_render.pyRendering HTML email safely.