Table of Contents

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.