# Repairing a workspace with no primary mailbox

A workspace with no primary mailbox has no fallback sender, so anything Connect starts rather than answers has nowhere to leave from. If a mailbox is visible, the repair is to give one the role. If the mailbox has become invisible — a row with an empty workspace stamp — no screen can fix it: the database policy refuses that write, and the repair runs as the schema owner during migration.

- **Status:** Available
- **Audience:** both
- **Channels:** email
- **In the app:** #/mailboxes, #/needs-you
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/primary-mailbox-repair/

## The flow, stage by stage

1. Trigger — a first message needs a sending mailbox and role resolution returns nothing.
2. External event — usually an outreach message, a follow-up falling due, or an administrator noticing an empty Mailboxes screen.
3. Authentication and workspace resolution — the workspace is entered first; every lookup that follows is scoped to it.
4. Ingest — the mailbox list is read through the one scoped query, which is also the query that cannot see an unstamped row.
5. Canonical record — no primary is found; the work is held rather than sent from an arbitrary address.
6. Classification and reasoning — the gap is classified as an operational problem, not as a customer problem, so no reply is improvised.
7. Knowledge, memory and rules — untouched; nothing about the business changed.
8. Autonomy and approval — the refusal is a decision and is recorded as one, with its reason.
9. Action through a provider — none. Refusing to send is the action.
10. Result — the item appears in Needs You with the cause named.
11. Relationship, timeline and memory — the recipient's timeline gains nothing, because nothing reached them.
12. Audit, usage and Needs You — the refusal is in the decision log, no allowance is spent, and the queue entry drains by itself once a primary exists.

Holding the work is the point. Sending from whichever mailbox happens to be available would put a business's first contact with someone under an address nobody chose, and would bind that conversation to it permanently.

## How the gap announces itself

| What you notice | What it means |
|---|---|
| Outreach and follow-ups stop leaving, replies still work | Replies inherit their mailbox from the thread; only new conversations need the fallback |
| A Needs You entry naming the missing primary | Detection worked; the repair has not been done yet |
| The Mailboxes screen is empty although mail is arriving | The rows exist but carry no workspace stamp, so no scoped list can see them |
| Reconnecting a mailbox produces a second entry for one address | The uniqueness check could not see the first row either |

The third and fourth rows are the case worth reading twice. An empty stamp matches no scope and no row-level security policy, so the row is invisible rather than missing: by-id actions against it keep working, mail keeps being fetched, and every list omits it in silence. Two connected Gmail identities can disappear from the screen this way with no warning and no error to search for.

## The two repairs

1. Open the Mailboxes screen. If a mailbox that can send is listed, give it the primary role.
   - Result: Held work resumes on the next tick and the Needs You entry drains by itself. This is the repair in most workspaces.
2. If no mailbox is listed but mail is plainly arriving, stop and do not reconnect anything.
   - Result: You avoid writing a second row for an inbox that already has one — the mistake that turns an invisible mailbox into a duplicated one.
3. Ask for the workspace to be restarted so the boot-time migration runs.
   - Result: `migrate.backfill_canonical_workspace` runs as the schema owner and stamps the orphaned rows. They reappear in every list, and the uniqueness check can see them again.
4. Re-open the screen and confirm one row per real inbox before assigning the primary role.
   - Result: You are assigning the role to the row the threads are already bound to, rather than to a copy.

> **Careful** Do not try to force the stamp from a screen or an API call. The policy's `WITH CHECK` refuses the write, and code that pushed past it would turn a quietly invisible row into a server error on a list that used to load. The read side was widened to see such rows; the write side was deliberately not.

## Why a request cannot do it

Every ordinary request runs under a role that is bounded by the workspace it entered. Row-level security is not a filter on reads alone: its check clause also governs what a row is allowed to look like after a write. A request cannot hand a row a workspace stamp, because from the database's point of view that is a request claiming a row it was never able to see.

Repair therefore belongs to the one actor that legitimately owns the shape of the schema, running at boot, before any session exists. That is slower than a button and it is the correct trade: the alternative is an endpoint whose job is to move rows between workspaces, which is precisely the capability three layers of isolation exist to deny.

## After the repair

Confirm three things rather than one. That each real inbox appears once; that the mailbox holding the primary role can actually send, which its health verdicts answer and its connection state does not; and that the held work has drained — the Needs You entry clears itself when its cause clears, so an entry still sitting there means the cause has not.

If duplicates survive the restart, do not delete one on instinct. Threads are bound to a specific mailbox row, and removing the wrong one strands the conversations that point at it. Escalate with the address and the count of rows you can see.

## Questions

### Why do replies still work when the primary mailbox is missing?

A reply takes its mailbox from the thread it answers, which was decided when the mail arrived. Only work Connect starts — outreach, a follow-up, a first contact — needs the fallback, which is why the symptom looks selective.

### Can I just reconnect the mailbox to bring it back?

That is the move to avoid when the list is empty but mail is arriving. The uniqueness check cannot see an unstamped row either, so reconnecting writes a second row for one real inbox. Get the rows stamped first, then look.

### Does the repair touch message history?

No. It gives existing rows the workspace stamp they should always have had. Threads, messages, drafts and contacts are untouched, and nothing is re-fetched from the provider.

## Related

- [The primary mailbox](https://connectbyjbrh.com/docs/email/primary-mailbox/)
- [Repairing a broken mailbox, end to end](https://connectbyjbrh.com/docs/workflows/mailbox-repair/)
- [Assigning a mailbox role, end to end](https://connectbyjbrh.com/docs/workflows/mailbox-role-assignment/)
- [A row with no workspace stamp is invisible, not missing](https://connectbyjbrh.com/research/invisible-not-missing/)
- [Three independent layers of tenant isolation](https://connectbyjbrh.com/research/three-layers-of-isolation/)

## What this page is based on

- CHANNELS.md §1 — the three defects, and repair as the schema owner
- GENERAL.md §2 and §3 — three isolation layers and the workspace stamp
- `backend/app/migrate.py` — `backfill_canonical_workspace`
- Connect capability registry (docs-source/facts.py) — `rls_isolation`
