Relationships for the Owner and for a customer
There is one implementation. relationship_console answers a question about a relationship, and two thin routers put it in front of two audiences: the operator API for the Owner, /api/customer/ui/… for a customer after tenantAdapt rewrites the path. The customer surface renames it — a customer's people are the Owner's relationships — and nothing else differs.
Where the two paths diverge, and where they do not#
| Owner | Customer | |
|---|---|---|
| Session | customer_mode false | customer_mode true |
| Workspace | owner-jbrh | ws_… |
| Path | /api/operator/…, /api/… | /api/customer/ui/… |
| How it gets there | Direct | tenantAdapt() rewrites the path |
| What answers it | relationship_console | relationship_console |
The last row is the important one. A capability that exists for one audience and not the other is a defect here, not a product decision — the only deliberate differences are commercial, where a customer's use is bounded by their plan's allowances and the Owner has no plan.
The rename is written down#
ALIASES in the audience parity suite records that a customer's people are the Owner's relationships, in the same way it records that knowledge-sources are the knowledge-bank. The suite reads the flattened route table out of the running application and compares every Owner capability against every customer one, so a rename is a fact the tests hold rather than a convention somebody remembers.
KNOWN_GAPS lists what a customer still cannot reach, each with a reason. It may shrink and it must never grow: adding a capability for one audience only fails the suite, and so does leaving a closed gap on the list.
Why a request from a customer can still fail#
tenantAdaptdid not recognise the path- Anything it cannot rewrite becomes
/api/customer/ui/blockedand answers 403. A new Owner route without a rewrite lands here. customer_safedid not allow it- An allowlist in
main.pyrefuses any/api/*a customer session may not call, independently of the rewrite. - The workspace kernel filtered it
- Records are scoped by
workspace_idbefore the query runs — a row from another workspace is not refused, it is not there. - Row-level security refused it
- The third enforcement, in the database itself, which holds even if the two above were bypassed.
A feature that works for the Owner and returns 403 for a customer is almost always one of the first two, and almost never the feature. That is worth knowing before anybody starts reading relationship code looking for a bug that is not in it.
Two things that are correctly Owner-only#
- Verifying customer payments. Running the platform, not using Connect.
- JBRH's own pricing. The same reason.
- Website enquiries addressed to JBRH.
connect_inquiriesis a control-plane table outside every workspace. A customer asking why an enquiry is not in their workspace is seeing isolation work.
None of those is a relationship capability. Within this section, what the Owner can do a customer can do — including merging, which is a decision each workspace makes for its own records.
Questions#
Why does the customer screen use different words?
Because *relationships* is how the operator talks about the businesses it serves, and *people* is how a business talks about the humans it deals with. The records and the code are identical; the label is chosen for the reader.
Is there a reduced customer version of the relationship screens?
No, and building one would be the defect. New behaviour lives in the shared module and each router adds only the route — a second implementation for the other audience is what the parity suite exists to catch.
How would I know if a capability was missing for customers?
The parity suite enumerates rather than asserts: it compares every Owner capability against every customer one and fails on a new one-audience capability, and on a gap that has since been closed but is still listed.