# A row with no workspace stamp is invisible, not missing

A record that is not on the screen has not necessarily gone anywhere. In a workspace-scoped system the commonest cause is a predicate that excludes it — most often a stamp of `''`, which matches no scope and no row-level security policy. The signature is an asymmetry: every list omits the row, and every action by id on it still works. That distinction decides the repair.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/invisible-not-missing/

## Three words that are not synonyms

**Missing** — The row does not exist. Something failed before it was written, or it was hard-deleted. This is the only one of the three where a backup is part of the answer.
**Deleted** — The row exists and a person put it out of sight — `threads.deleted_at` and its equivalents on `calls`. Reversible, deliberate, and the person's own decision to make.
**Invisible** — The row exists, holds live data, and is excluded by a predicate. Nothing failed and nothing raised. Restoring a backup here creates a second copy of a record that was never gone.

Choosing between them by instinct is how one hidden row becomes two live ones. The case that made this concrete: two connected mail identities vanished from a screen with no duplicate warning, no error and no failing request — and the uniqueness check that should have prevented a reconnection could not see them either, so connecting again would have written a second row for one real inbox, both holding credentials.

## The triage, in about a minute

1. Fetch the record by its identifier — a link, a permalink, an action that takes an id.
   - Result: If it answers, the row exists and this is a visibility problem. Nothing needs restoring, and the incident is now about a predicate rather than about data loss.
2. Ask, read-only, how many rows in that table carry an empty stamp rather than a NULL one.
   - Result: The two are different faults. A NULL usually means a column was added without a backfill; an empty string usually means something wrote a default it never intended. The counts tell you which story you are in.
3. Check the uniqueness path for the same key.
   - Result: If the key reads as free while a hidden row holds it, a duplicate is one user action away and the screen is actively inviting one. That is the urgent half of the incident.
4. Only then consider a backup, and only if step one found nothing.
   - Result: A restore against an invisible row is how a support ticket becomes a data problem.

> **Careful** Do not repair by re-creating the record. Two rows for one real thing — one visible, one not, both holding credentials — is a worse state than the one you started with, and neither of them is obviously wrong afterwards.

## What else hides a row that exists

| Cause | How it presents | Where to look |
|---|---|---|
| Empty workspace stamp | Lists omit it; by-id works; no error anywhere | The read-only count per scoped table |
| A soft delete | Gone from the default view, present in the deleted view | `deleted_at` on the record |
| A capability the session may not call | The screen is empty or a request is refused with 403 | The `customer_safe` allowlist and `tenantAdapt` |
| A count that stopped at the page size | Rows exist, the total is wrong, a later page is empty | [Headline numbers that stop at the page size](/research/counting-past-the-limit/) |
| A table absent from `SCOPED_TABLES` | The opposite symptom — rows appear that should not | The scoped-table list, and whether a policy exists at all |

The last row is worth sitting with. The same class of oversight produces both invisibility and over-visibility, depending on which half is missing: a stamp that matches nothing hides a row from its owner, and a table that nothing filters shows rows to everybody.

## Why the fix is not a quick update

The instinct is to stamp the row as soon as something notices. That write is refused: the policy's `WITH CHECK` will not allow a row to be updated into a scope it is not already in, so an attempt from a request turns a quietly hidden row into a server error on somebody's screen. The permission that would allow it is not one a request should hold.

So the repair belongs to the schema owner and runs where schema changes already run — `migrate.backfill_canonical_workspace`, on every boot, on the connection that legitimately writes across scopes. The mechanics of that repair are written up in [a row with an empty workspace id](/research/workspace-stamp/); this page is about the decision that comes before it.

- A `NOT NULL` constraint prevents the next one and finds none of the existing ones. Repair first, constrain afterwards, or the migration fails on the rows you were trying to fix.
- The read-only count proves how many rows are hidden now. UNKNOWN: it cannot say when each was written or by which path, because nothing recorded a fault at the time.
- Control-plane tables cannot hold this defect — platform identity, sessions, billing and website enquiries are outside every workspace deliberately.

## Questions

### Is the data lost?

Almost never, in this failure. The row is present and complete; a predicate excludes it. Fetching it by identifier settles the question in seconds, and if that works, no backup is involved in the repair.

### Why was there no error?

Because from the query's point of view nothing went wrong. It asked for rows in this workspace and was correctly told there were none. Both the ORM filter and the database policy agreed, which is why a second enforcement layer does not catch this one.

### Can this happen to conversations or contacts as well as mailboxes?

Every table in `SCOPED_TABLES` can hold it — the defect is in the stamp, not in the record type. It surfaced on mailboxes because a mailbox is something people count, so two of them going quiet was noticed.

## Related

- [Data that is invisible rather than missing](https://connectbyjbrh.com/research/workspace-stamp/)
- [Three independent layers of tenant isolation](https://connectbyjbrh.com/research/three-layers-of-isolation/)
- [Headline numbers that stop at the page size](https://connectbyjbrh.com/research/counting-past-the-limit/)
- [Why 'connected' is not enough to prove mailbox health](https://connectbyjbrh.com/research/mailbox-health-beyond-connected/)
- [Troubleshooting](https://connectbyjbrh.com/docs/troubleshooting/)
- [Security and isolation](https://connectbyjbrh.com/docs/security/)

## What this page is based on

- `docs-source/sources/CHANNELS.md` §1 — the invisible mailbox rows, the uniqueness check and the boot repair
- `docs-source/sources/GENERAL.md` §3 — `SCOPED_TABLES`, `CONTROL_PLANE_TABLES` and where row-level security comes from
- Connect capability registry (docs-source/facts.py) — `rls_isolation`, `mailbox_health`
