Authorization, expressed once
The presence of a link encodes permission. The server decides per response; the frontend renders off can() and never re-derives your auth rules.
Hypermedia controls (HATEOAS) are the REST maturity level most teams never reach. Affordant gets you there — the server declares the actions it offers; your UI renders off them, with no authorization re-implemented in the frontend.
Your frontend never builds a URL, never picks an HTTP verb, never duplicates an authorization rule. It asks three questions:
import { can, actionFor, follow } from 'affordant'
if (can(order, 'cancel')) { // 1. What is the server offering me?
await follow(actionFor(order, 'cancel')!, { // 2. Where / how? 3. Do it.
token: () => localStorage.getItem('token'),
body: { reason: 'changed my mind' },
})
}If the backend stops offering an action — not authorized, wrong state, feature off — the button disappears. No frontend deploy.
The server declares those same affordances once, gating each on authoritative state. When when is false, the rel is never emitted — so can() returns false on the client.
import { resource } from '@affordant/server'
resource(order)
.self(route('orders.show', order.id))
.action('cancel', route('orders.cancel', order.id), {
method: 'POST',
when: caller.id === order.ownerId && order.status !== 'shipped',
})
.build()One contract, never two implementations to keep in sync. See the packages for the whole family.