Serve several kinds of caller from one route
Goal: answer one route for more than one kind of party — a richer view for a signed-in user, a public view for everyone else — without splitting the route or hand-rolling the “try auth A, else B” branching.
Name an ordered sum of peer actors on the by clause with |. The boundary
tries each in declared order and binds the first that verifies; the body
matches on which one it was.
context api
type UserId = String where NonEmpty
type Note = { id: String, owner: String }
actor User { auth = Bearer(secret = "AUTH_JWT_SECRET"), identity = UserId }
service api from http { on GET("/notes/:id") by who: User | Visitor (id: String) -> Effect[HttpResult[Note]] { match who { User(u) => Ok(Note { id: id, owner: u }) Visitor => Ok(Note { id: id, owner: "public" }) } }}Each arm binds that actor’s identity directly — User(u) gives u : UserId
(the arm already names the actor, so there is no .identity step); a party with
no identity, like Visitor, binds nothing. If no member verifies, the route
fails closed with 401.
The rules
Section titled “The rules”A sum is checked for reachability — decidably, at the scheme level:
- It needs a binder — the body learns which party verified by matching it.
- Peers are distinguished by scheme, so no two members may share one (
User | Visitor✓; twoBeareractors ✗). - A catch-all comes last.
Visitor(schemeNone) accepts everyone, so anything after it is unreachable — writeUser | Visitor, neverVisitor | User. - Refinements are not members.
User | Adminis rejected — everyAdminis aUser, so the arm is dead. Narrow inside an arm instead (see authorisation invariants). - The body
matchmust be exhaustive over the members.
Mixing a header and a body member
Section titled “Mixing a header and a body member”Members can verify different ways — a header (Bearer) and a body
(Signature) — in one route. The boundary reads the body once, tries each
member against the material in hand, and parses the body from the same bytes:
context api
type UserId = String where NonEmpty
type Event = { id: String }
actor User { auth = Bearer(secret = "AUTH_JWT_SECRET"), identity = UserId }actor Hook { auth = Signature(secret = "WH_SECRET", header = "X-Signature") }
service api from http { on POST("/ingest") by who: User | Hook (body: Event) -> Effect[HttpResult[String]] { match who { User(u) => Ok(u) Hook => Ok(body.id) } }}Next: add an authorisation invariant to a member, or read the reference.