# Resolving which workspace a request belongs to

Resolution happens once, in the middleware, before any router sees the request: the session names one workspace and the request is answered inside it or not at all. Enforcement then happens three more times — an allowlist in front of the application, a filter on every query it builds, and a policy inside the database on the statement it receives.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/workspace-resolution-flow/

## The flow

1. Trigger — any request arrives: a screen loading, an action, a background tick doing work for one workspace.
2. User or external event — the browser presents its session cookie, or an integration presents its key. Neither carries a workspace of its own choosing.
3. Authentication and workspace resolution — the session record is read and the workspace is fixed. This is the only moment scope is decided, and nothing later can change it.
4. Ingest — the path is checked against what this audience may call. An unrecognised or unpermitted path is refused here, before any handler runs.
5. Canonical record — the handler asks for records. Every scoped table carries a workspace column, and the filter is applied whether or not the handler remembered.
6. Reasoning — the engine reasons inside the resolved scope. It cannot ask a question of another workspace's data, because there is no query it can build that reaches one.
7. Knowledge, memory and rules — all scoped records. Memory at every tier, knowledge sources, autonomy rules: each belongs to one workspace.
8. Autonomy and approval — resolved within the workspace, at the narrowest applicable scope.
9. Action through a provider — the credentials used are the workspace's own, sealed, and resolved through the same scope.
10. Result — the database applies row-level security to the statement it actually receives, as the last line rather than the first.
11. Relationship, timeline and memory — everything written carries the workspace stamp, so tomorrow's reads resolve the same way today's writes did.
12. Audit, usage and Needs You — the decision record, the usage ledger and the queue are all scoped records too. There is no cross-workspace view of any of them.

## Deciding, then enforcing

It is worth separating the two halves, because they answer different questions. **Deciding** happens once and produces a single answer: this request belongs to this workspace. **Enforcing** happens repeatedly and answers a narrower question each time: does this specific read or write stay inside that answer.

| Where | What it checks | What it cannot see |
|---|---|---|
| In front of the application | Whether this audience may call this capability at all | Anything about rows — it has no opinion inside a permitted call |
| In every query the application builds | That the workspace condition is present, without the handler adding it | A read that never becomes a query, or a table nobody registered as scoped |
| In the database | The statement as received, whatever produced it | A table whose policy was never applied, or a row with an empty stamp |

[Proving a workspace is isolated, end to end](/docs/workflows/isolation-verification/) is how to check the three rather than take them on trust, and [Three independent layers of tenant isolation](/research/three-layers-of-isolation/) is the argument for why there are three.

## What is deliberately not scoped

A small set of tables sits outside every workspace on purpose, and knowing which is part of understanding the model rather than an exception to apologise for: platform identity — the accounts people sign in with — sessions, billing records, and enquiries addressed to the operator through the public site.

The reasoning is that each of them exists *before* a workspace does, or *across* several. An identity that belonged to one workspace could not be the same person in two; an enquiry from a stranger belongs to nobody's workspace by definition. What matters is that the list is short, written down, and does not grow casually — a table that quietly joins it stops being protected by any of the three enforcement points.

## Where it goes wrong

**A new table with a workspace column and no policy** — Scoped by the application only. This is the failure the third layer exists for, and it is a release-time omission rather than a design fault.
**A read by identifier that never becomes a query** — Bypasses the second layer entirely, and therefore the third as well. The reason every by-identifier route goes through a scoped lookup.
**A capability with no route for one audience** — Not a scoping fault at all — a routing gap, which shows as a refusal. See [Routing a request for the Owner or a customer](/docs/workflows/owner-tenant-routing/).
**A row whose stamp is empty** — Matches no scope and no policy, so it is invisible rather than protected. See [Data that should be here is not](/docs/troubleshooting/data-not-visible/).

## What this means for a background task

Work that nobody is watching — a tick draining follow-ups, a fetch, a scheduled brief — resolves a workspace the same way and then stays inside it. That is why a task cannot process two workspaces in one pass: each pass belongs to one, which is slower to describe and much easier to reason about when something has gone wrong at three in the morning.

## Questions

### Can a request change workspace part-way through?

No. Scope is fixed in the middleware before any handler runs, and there is no supported way to widen it afterwards. A person who belongs to two workspaces changes by changing session.

### Is the platform operator's workspace special?

It resolves through the same mechanism and is subject to the same three enforcement points. What is genuinely operator-only is the small set of surfaces for running the platform rather than using Connect — payments, pricing and public enquiries — and those are separate surfaces, not a wider scope.

### What happens if no workspace can be resolved?

The request does not proceed. There is no default workspace and no fallback, because a fallback is a way of answering a question you could not answer, which is precisely the mistake this design is arranged to avoid.

## Related

- [Which workspace you are in](https://connectbyjbrh.com/docs/account/workspace-resolution/)
- [Proving a workspace is isolated, end to end](https://connectbyjbrh.com/docs/workflows/isolation-verification/)
- [Three independent layers of tenant isolation](https://connectbyjbrh.com/research/three-layers-of-isolation/)
- [Multi-tenancy in Connect](https://connectbyjbrh.com/docs/security/multi-tenancy/)
- [Routing a request for the Owner or a customer](https://connectbyjbrh.com/docs/workflows/owner-tenant-routing/)

## What this page is based on

- Connect source pack section 2 — the request flow: authentication, allowlist, workspace scope, router, kernel, row-level security (`docs-source/sources/GENERAL.md`)
- Connect source pack section 3 — scoped tables and the control-plane exception (`docs-source/sources/GENERAL.md`)
- Connect capability registry (`docs-source/facts.py`) — `workspace_resolution`, `rls_isolation`
