# Data that is invisible rather than missing

An empty workspace stamp does not delete a row; it hides it. `''` matches no scope and no row-level security policy, so every *list* omits the row in silence while every *by-id* action on it still works. That asymmetry is the diagnosis — a record you can open from a link and cannot find in the list it belongs to — and it is also why the repair cannot be a write from a request.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/workspace-stamp/

## What it looks like from the screen

Two connected Gmail identities disappeared from the Mailboxes screen. Nothing raised. There was no duplicate warning, no error toast, no failing request and no log line, because from the query's point of view nothing had gone wrong: it asked for rows in this workspace and was correctly told there were none.

The dangerous part is the second-order effect. `find_by_key`, the check that stops one real inbox being connected twice, was a scoped query too. It could not see the hidden rows either, so it reported the address as unconnected — and reconnecting would have written a *second* row for one real inbox, with the first still there, still invisible, and still holding live credentials.

| Operation | Sees it? | Why |
|---|---|---|
| List for the active workspace | No | The filter compares `workspace_id` to the scope in force, and `''` equals nothing |
| Row-level security in PostgreSQL | No | The policy compares the same way, so the database agrees with the ORM rather than catching it |
| Fetch by primary key | Yes | The identifier is exact and the row is genuinely there |
| Uniqueness check by address key | No, before the fix | It was a scoped list, so it answered 'not connected' about a connected mailbox |
| A write that stamps the row correctly | Refused | The policy's `WITH CHECK` rejects it — the row is not in scope to be updated into scope |

## Finding them: one query, deliberately read-only

The list and the uniqueness check now both ask the one query that can see such rows — and it is **read-only** on purpose. Being able to see a row and being allowed to change it are separate permissions here, and collapsing them is how a diagnostic becomes an outage.

1. Count rows whose `workspace_id` is empty rather than absent, per scoped table.
   - Result: Empty and NULL are different failures with different causes: a NULL usually means the column was added without a backfill, an empty string usually means something wrote a default it never meant to.
2. Check whether the by-id path still works for one of them.
   - Result: If it does, you have confirmed invisibility rather than loss, and nothing needs restoring from a backup.
3. Check the uniqueness path for the same key.
   - Result: If the key reads as free while a row holds it, a duplicate is one user action away and the screen is actively inviting it.

> **Careful** Do not repair by re-creating the record. A second row for one real inbox is a worse state than one hidden row: both hold credentials, only one is visible, and neither is obviously wrong.

## Why the repair happens at boot and not in a request

The instinct is to stamp the row the moment something notices. That write is refused: the policy's `WITH CHECK` will not let a row be updated into a scope it is not already in, so an attempt from a request turns a quietly missing row into a 500 on somebody's screen. The permission that would allow it is not one a request should be holding.

So the repair belongs to the schema owner, and it runs where schema changes already run: `migrate.backfill_canonical_workspace`, every boot. `migrate.py` is the file that legitimately owns the shape of the database, it already runs on the connection that may write across scopes, and running every boot means the fix is applied to whatever state a deploy finds rather than once by hand and never again.

> **Note** This is the same reasoning as row-level security itself. `create_all` makes a table at boot, but the policy comes from `tools/pg_harden.py`, an operational script — so a new scoped table needs its policy applied in the same release, and a table carrying `workspace_id` that is missing from `SCOPED_TABLES` is scoped by nothing at all.

## The general case, and what this does not cover

- Every table in `SCOPED_TABLES` can hold this defect. `CONTROL_PLANE_TABLES` cannot, because they are deliberately outside every workspace — platform identity, sessions, billing and `connect_inquiries`.
- A `NOT NULL` constraint prevents the next one and finds none of the existing ones; worse, adding it to a table that already holds them fails the migration. Repair first, constrain afterwards.
- The read-only query proves how many rows are hidden right now. It cannot say how long they were hidden, or what was decided while they were.
- There is no figure here for how often this occurred across the corpus of scoped tables: UNKNOWN. Two mailbox rows are the measured instance.

## Questions

### Is the row actually lost?

No. It is present, intact, and reachable by its identifier — which is why every by-id action kept working while the list showed nothing. Nothing needs to be restored; the stamp needs to be corrected by the one actor allowed to correct it.

### Why not repair it from an administrative session?

Because the same `WITH CHECK` refuses writes under the platform administrative role too. Seeing across scopes and writing across scopes are separate rights, and the second one lives with the schema owner in `migrate.py` rather than with anything that serves a browser.

### How do you know none are left?

By running the same read-only count as a check rather than as an investigation. It is cheap, it has a correct answer of zero, and it belongs beside the other boot-time assertions — see [Migrations that run on every boot](/research/boot-time-migrations/).

## Related

- [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/)
- [Migrations that run on every boot](https://connectbyjbrh.com/research/boot-time-migrations/)
- [Security and isolation](https://connectbyjbrh.com/docs/security/)
- [Email in Connect](https://connectbyjbrh.com/docs/email/)

## What this page is based on

- `docs-source/sources/CHANNELS.md` §1 — the three mailbox defects, including the invisible row, `find_by_key` and the read-only query
- `docs-source/sources/GENERAL.md` §3 — `SCOPED_TABLES`, `CONTROL_PLANE_TABLES`, and RLS from `tools/pg_harden.py`
- `docs-source/facts.py` — `CAPABILITY_STATUS.rls_isolation`
