Access-aware UI
Present auth, role, plan, and limit state without duplicating gateway authorization.
A subscriber can be unable to use a control for different reasons. Keep those reasons separate so the UI offers the right remedy:
| Axis | Question | UI response | Security boundary |
|---|---|---|---|
| Authentication | Is there a current subscriber session? | sign in | session validation |
| Member permission | May this member perform the action? | hide, disable, or request access | gateway route permission |
| Plan/subscription | Does the subscriber's pinned plan grant the route and is access active? | choose/upgrade/repair subscription | gateway plan and subscription check |
| Dynamic limit | Is this request within quota, rate, capacity, concurrency, spend, or adaptive limits? | retry, reduce, queue, top up, or upgrade | gateway admission decision |
The frontend can present observed state, but the route call is authoritative because plan, role, subscription, and usage can change after render.
Compose authentication and permission presentation
import {
AccessDenied,
PermissionGate,
RequireAuth,
} from "@farthershore/farthershore-js/components";
export function CreateReportButton() {
return (
<RequireAuth fallback={<p>Sign in to create reports.</p>}>
<PermissionGate
permission="reports:create"
mode="denied"
fallback={<AccessDenied requiredPermission="reports:create" />}
>
<button>Create report</button>
</PermissionGate>
</RequireAuth>
);
}
PermissionGate uses the server-resolved current member claim. It is a
presentation primitive, not a client-side grant database.
Handle the actual call
import {
FartherShoreApiError,
LimitExceededError,
retryWhileThrottled,
} from "@farthershore/farthershore-js";
async function createReport(input: unknown) {
try {
return await retryWhileThrottled(() => fs.route.post("/v1/reports", input));
} catch (error) {
if (error instanceof LimitExceededError) {
showLimitNotice(error);
return;
}
if (error instanceof FartherShoreApiError) {
showStableDeny(error.code);
return;
}
throw error;
}
}
Only retry when the denial envelope says the request is retry-safe. Preserve an application idempotency key for writes and bound retry attempts. Quota/spend upgrade reactions and capacity reductions are not fixed by blind backoff.
LimitNotice maps the current limit class to the correct explanation.
FsLimitBoundary can show a global prompt for a caught LimitExceededError.
Use UpgradePrompt only for plan or funding remedies; a permission denial should
offer access-request or administrator guidance instead.
Do not duplicate the contract
Plan grants are route/group refs in business/. Permission constraints and
subject requirements also live on the route contract. Do not create a second
hard-coded client feature map and treat it as authority.
It is reasonable to use bootstrap, entitlement, resource-limit, usage, and permission hooks to reduce dead-end interactions. Every mutation must still handle a deny from the gateway, and backend record queries must still use the verified organization.
Avoid content flashes
- Hold protected content while auth is loading.
- Hold permission-gated content until
authzLoadedorusePermissionGate()resolves. - Do not optimistically reveal plan-restricted functionality from a stale local cache.
- After plan, role, or organization changes, let the SDK invalidate/refetch its resources instead of manually mutating several copies of access state.
These rules keep the UI responsive without moving authorization into the browser.