UI API
/api/ui — what a storefront calls
Everything a signed-in user does. The only surface that is scoped to a customer, and the only one where authorization depends on the user's role within that customer.
Who calls it
A storefront running in a browser — the reference one, or a bespoke one. Nothing else should.
| Authentication | Cookie session. See Authentication |
| Authorization | PortalUser on every controller, then a customer role per action |
| Scope | The user's active session decides which customer the request acts for |
| Contract stability | Internal. It moves when the storefront moves |
Two layers of authorization
Every controller carries PortalUser, which establishes a signed-in portal user. That is not enough
on its own, because a user's rights depend on the customer they are currently acting for.
So actions additionally require a customer role:
| Role | Can |
|---|---|
Viewer |
Read — browse, see a basket, see orders, see the account |
Purchaser |
Everything a viewer can, plus change a basket and check out |
Admin |
Everything a purchaser can, plus manage that customer's own users |
Read endpoints require Viewer; basket mutation and checkout require Purchaser; managing users
within a customer requires Admin.
Why two layers rather than one. Being signed in and being allowed are different questions, and the second one changes when a user switches customer while the first does not. Collapsing them would mean re-issuing the session on every switch.
Scope is the whole point of this surface
A request here runs scoped: database queries are automatically filtered to the customer from the user's active session, and rows written are stamped with it. A handler does not filter by customer, and cannot forget to.
This is the only surface where that happens. It is also why the session — rather than a claim in the cookie — decides the customer: a user may belong to several, and switching must not require a new cookie. The mechanics are on Database.
Important
If you add an endpoint here and find yourself writing where AccountNo == …, stop. Either the
scoping is doing it for you and you are duplicating it, or you are on the wrong surface.
What is under it
| Route | For |
|---|---|
api/ui/bootstrap |
Everything the storefront needs on first load, in one call |
api/ui/catalogue |
Search, browse, product detail |
api/ui/basket |
The basket and everything it costs |
api/ui/checkout/{basketId} |
Stage, proceed to payment, place |
api/ui/orders |
Order history and line-level status |
api/ui/customer |
The customer's own account, addresses and users |
api/ui/inventory |
Stock for a set of products |
api/ui/address |
Address lookup and suggestion |
api/ui/cms |
Editable content blocks |
api/ui/bootstrap exists because a storefront otherwise opens with a burst of parallel calls before
it can render anything. One call is easier to cache, easier to reason about, and easier to make fast.
Composition happens here, not in the domains
Most interesting endpoints on this surface need several domains at once — a priced basket needs pricing, tax, stock, freight and the catalogue. That assembly happens in a broker, in the host, not inside a domain. See Architecture.
Errors
Domain failures become problem details with an appropriate status. A basket that cannot be checked out
is not an error — it returns 200 with the reasons attached, because "you cannot ship to that
address" is an ordinary answer to an ordinary question, not a transport fault.