# Customer-safe

`customer_safe` is the allowlist in Connect's HTTP middleware that decides which `/api/*` paths a customer session is permitted to call. A path it does not permit is refused with 403 before any handler runs. It is a route control, not a data control, and it is the usual reason a screen that works for the Owner returns nothing for a tenant.

- **Status:** Available
- **Audience:** both, developer
- **In the app:** #/account
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/glossary/customer-safe/

## What it does, and the order it does it in

Connect is one codebase serving two audiences. An Owner session calls `/api/operator/…` and `/api/…` directly; a customer session calls `/api/customer/ui/…`. Every request passes the same middleware in the same order — authentication, then `customer_safe`, then workspace resolution — and only then reaches a router.

The allowlist sits in the middle of that sequence on purpose. It runs after Connect knows who is asking and before Connect has done any work: a refusal costs one comparison and touches no data. Because it is an allowlist rather than a deny list, a route added without being considered is refused to customers by default. That is the safe direction to fail, and it is also why a new feature can be complete, tested and quietly unreachable for every tenant.

## Customer-safe versus tenantAdapt

These two produce the same 403 on the same screen and are not the same control. `tenantAdapt`, in the browser, rewrites an Owner path into its customer equivalent; anything it does not recognise becomes `/api/customer/ui/blocked`, which answers 403 by design. `customer_safe`, on the server, judges the path that actually arrives.

**The rewrite never happened** — A path `tenantAdapt` does not know is turned into `/api/customer/ui/blocked`. The request that leaves the browser is already addressed to a refusal. Fixed by adding the rewrite.
**The rewrite happened and the server said no** — A correctly rewritten `/api/customer/ui/…` path that the allowlist does not permit. Fixed by permitting the route — deliberately, after deciding a tenant should reach it.
**Neither: the route is genuinely Owner-only** — Verifying customer payments, JBRH's own pricing, and enquiries sent to JBRH through its website. These concern running the platform rather than using Connect, and the refusal is correct.

Telling the first two apart takes one look at the request path. If it ends in `blocked`, the browser decided; if it is a real customer path, the server decided. Nothing else about the symptom distinguishes them.

## What it is not

It is not permission or role checking. A workspace member's rights inside their own workspace are settled elsewhere; `customer_safe` only answers whether this *class* of session may address this route at all.

It is not isolation. Two customers are kept apart by the workspace kernel and by row-level security, both of which run below this and would still hold if the allowlist were wrong. Permitting a route does not widen what a workspace can see through it.

And it is not a feature gate. Connect has one feature set: every capability belongs to both audiences over the same implementation, and what differs commercially is the allowance a plan carries. An allowlist entry that is missing is an oversight to be closed, not a product tier.

> **Note** The parity rule is enumerated rather than asserted. A test reads the route table out of the running application and compares every Owner capability against every `/api/customer/**` one. `ALIASES` records where the customer surface renames something — a customer's *people* are the Owner's *relationships*, its *knowledge sources* are the *knowledge bank*. `KNOWN_GAPS` lists what a tenant still cannot reach, each entry with a reason; the suite fails when that list grows, and also when a gap on it has quietly been closed.

## Reading a 403 from it

1. Confirm the audience. Reproduce the action in a customer session and in the Owner's.
   - Result: If both fail, this is not the allowlist — look at the feature. If only the customer fails, continue.
2. Read the path the browser actually requested.
   - Result: `/api/customer/ui/blocked` means no rewrite matched. A real customer path means the server refused it.
3. Report it as an audience asymmetry rather than as a broken screen, naming the exact action and the path.
   - Result: The fix is one route entry — either a rewrite or an allowlist line — rather than a change to the feature, so the report is short and the correction is small.

What you cannot do from a customer session is talk your way past it. There is no per-account exception and no setting on any screen; the allowlist is code, changed in a release, and that is what makes it worth trusting.

## Where to read more

- [The customer facade](/docs/security/customer-facade/) — the tenant-facing surface as a whole.
- [Authorisation](/docs/security/authorization/) — what a member may do once a route is reachable.
- [Owner and tenant routing](/docs/workflows/owner-tenant-routing/) — the same journey as a flow, end to end.
- [Workspace kernel](/docs/glossary/workspace-kernel/) — the row layer underneath.
- [Owner versus tenant](/docs/account/owner-vs-tenant/) — what actually differs between the two accounts.

## Questions

### Is customer-safe why a feature is missing from my workspace?

It is the most likely cause of a screen or panel that fails only for a tenant, and it is a fault rather than a boundary. Connect deliberately has no customer-only or Owner-only feature apart from the three platform-operation ones, so a capability a tenant cannot reach is either a missing rewrite, a missing allowlist entry, or a recorded gap with a written reason.

### Does permitting a route let a customer see another workspace's data?

No. The allowlist decides which routes may be called, not which rows come back. Two further layers narrow the answer to one workspace — the query kernel in the application and row-level security in PostgreSQL — and they apply to every permitted route without being asked.

### Why is a website enquiry addressed to JBRH not in my workspace?

Those enquiries live in a control-plane table that sits outside every workspace by design, so no tenant route reads them. Seeing them absent is isolation working rather than a fault — they are JBRH's own mail, not yours.

## Related

- [The customer facade](https://connectbyjbrh.com/docs/security/customer-facade/)
- [Authorisation](https://connectbyjbrh.com/docs/security/authorization/)
- [Routing a request for the Owner or a customer](https://connectbyjbrh.com/docs/workflows/owner-tenant-routing/)
- [Workspace kernel](https://connectbyjbrh.com/docs/glossary/workspace-kernel/)
- [The operator's workspace and a customer's](https://connectbyjbrh.com/docs/account/owner-vs-tenant/)
- [One product, two audiences](https://connectbyjbrh.com/docs/product/owner-and-customer/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §1 — the two audiences, `tenantAdapt`, the parity suite, `ALIASES` and `KNOWN_GAPS`
- `docs-source/sources/GENERAL.md` §2 — the middleware order a request passes through
- Connect capability registry (`docs-source/facts.py`) — `owner_tenant_routing`, `rls_isolation`, `website_enquiries`
