HATEOAS actions pattern — show only actions the user can take #11

Closed
opened 2026-08-06 18:15:58 +00:00 by ginjiruu · 2 comments
Owner

Question

How should the HATEOAS "show only actions the user can take" pattern look in the example app — and what does it need from the absorbed toolbelt/authz?

The rationale (from the authz deliberation): every app should only surface actions the current user can actually take — gray the ones they can't (with a tooltip), don't show buttons that will 403. It's good UX, and it should be the canonical pattern every example demonstrates.

ListRelations (BatchCheck over openfgav1) is the primitive that powers it: given (user, object, [relations]), return which relations hold, so the UI renders exactly the enabled actions. ttrpg already implements it for HATEOAS action computation (internal/authz/fga_authorizer.go ListRelations + generated {Type}RelationsForUser helpers); the example app doesn't yet.

Decisions this ticket resolves:

  1. The canonical UX pattern for the example (hide vs gray + tooltip; which actions: card create/edit/delete, column add/rename/reorder/delete, board delete, archive/restore).
  2. Whether ListRelations/BatchCheck gets a clean home in toolbelt/authz (or embeddedfga) — per the authz ticket, ScopeFunc and ListRelations are both being absorbed.
  3. How the generated fga.{Type}RelationsForUser helpers (from tools/fga-generate) and the HATEOAS computation fit the absorbed authz surface.
  4. What "actions available" the read model (KV) carries vs what's computed per-request.

This ticket graduates the fog from the authz ticket ("ListRelations left in fog") into a concrete example pattern.

Resolve with /grilling + /domain-modeling (prototype the UI if useful). On close, write the plan to docs/plans/. Per the map's deliverable shape, no example code lands with this ticket — implementation is deferred to the post-map clean rewrite.

Child of: "Toolbelt glue absorption — map: what the toolbelt absorbs from duplicated consumer glue" (#1).

## Question How should the HATEOAS "show only actions the user can take" pattern look in the example app — and what does it need from the absorbed `toolbelt/authz`? The rationale (from the authz deliberation): every app should only surface actions the current user can actually take — gray the ones they can't (with a tooltip), don't show buttons that will 403. It's good UX, and it should be the canonical pattern every example demonstrates. `ListRelations` (BatchCheck over `openfgav1`) is the primitive that powers it: given `(user, object, [relations])`, return which relations hold, so the UI renders exactly the enabled actions. ttrpg already implements it for HATEOAS action computation (`internal/authz/fga_authorizer.go` `ListRelations` + generated `{Type}RelationsForUser` helpers); the example app doesn't yet. Decisions this ticket resolves: 1. The canonical UX pattern for the example (hide vs gray + tooltip; which actions: card create/edit/delete, column add/rename/reorder/delete, board delete, archive/restore). 2. Whether `ListRelations`/BatchCheck gets a clean home in `toolbelt/authz` (or `embeddedfga`) — per the authz ticket, `ScopeFunc` and `ListRelations` are both being absorbed. 3. How the generated `fga.{Type}RelationsForUser` helpers (from `tools/fga-generate`) and the HATEOAS computation fit the absorbed `authz` surface. 4. What "actions available" the read model (KV) carries vs what's computed per-request. This ticket graduates the fog from the authz ticket ("ListRelations left in fog") into a concrete example pattern. Resolve with `/grilling` + `/domain-modeling` (prototype the UI if useful). On close, write the plan to `docs/plans/`. Per the map's deliverable shape, no example code lands with this ticket — implementation is deferred to the post-map clean rewrite. **Child of:** "Toolbelt glue absorption — map: what the toolbelt absorbs from duplicated consumer glue" (#1).
Author
Owner

RESOLVED — design settled via grilling.

Answer: actions are generated from the FGA model via a can_ convention; permission-only; routes and state stay app-side.

Eight decisions locked:

  1. UX is the designer's call; the platform provides the tools for both. Gray-with-reason is the best UX, but hiding is legitimate — the generated action set serves both (annotated actions render as hidden or grayed + tooltip).
  2. The can_ convention. A relation beginning with can_ is a capability action, 1:1 with an affordance, and must read as "can ". owner/member are composition roles, never actions. This is what makes codegen viable — no bundles to guess. Advisory to document in the fga-generate docs.
  3. Codegen, not runtime glue. tools/fga-generate emits, dependency-free (imports only context): Actions() (all can_* actions for a type) and ActionsForUser(ctx, lister, user, object) — the same set annotated {Rel, Allowed, Reason} from one ListRelations batch. No app-side computeActions; no glue.
  4. Action = {Rel, Allowed, Reason} — no Href/Method. Routes, labels, and widgets live in the renderer's rel→widget map. Dumb, templ-free.
  5. Permission-only. State conditionals ("already archived → show Restore") stay in the renderer. Declarative state verification is a new decision ticket → graduated #24.
  6. Authz seam (closes #2's open point): absorbed authz.ListRelations(ctx, user, object, relations) hides raw openfgav1 BatchCheck; the generated RelationLister stays in generated output; authz.Authorizer satisfies it.
  7. Read model: per-board KV buckets carry resource state; actions recompute per render against live FGA — permission changes reflect on the next push, no refresh.
  8. Grown model: board relations split per action (can_delete_board, can_add_column, …, can_archive_card, can_restore_card, …), all [user]-typed, seeded so granularity is visible (bob = member without can_delete_board).

Plan: docs/plans/hateoas-actions.md. Implementation deferred to the post-map clean rewrite per the map's deliverable shape. OpenAPI-driven routes left as map fog.

**RESOLVED — design settled via grilling.** **Answer: actions are generated from the FGA model via a `can_` convention; permission-only; routes and state stay app-side.** Eight decisions locked: 1. **UX is the designer's call; the platform provides the tools for both.** Gray-with-reason is the best UX, but hiding is legitimate — the generated action set serves both (annotated actions render as hidden *or* grayed + tooltip). 2. **The `can_` convention.** A relation beginning with `can_` is a capability action, 1:1 with an affordance, and must read as "can <verb> <noun>". `owner`/`member` are composition roles, never actions. This is what makes codegen viable — no bundles to guess. Advisory to document in the fga-generate docs. 3. **Codegen, not runtime glue.** `tools/fga-generate` emits, dependency-free (imports only `context`): `Actions()` (all `can_*` actions for a type) and `ActionsForUser(ctx, lister, user, object)` — the same set annotated `{Rel, Allowed, Reason}` from one `ListRelations` batch. No app-side `computeActions`; no glue. 4. **`Action = {Rel, Allowed, Reason}`** — no Href/Method. Routes, labels, and widgets live in the renderer's rel→widget map. Dumb, templ-free. 5. **Permission-only.** State conditionals ("already archived → show Restore") stay in the renderer. Declarative state verification is a new decision ticket → graduated **#24**. 6. **Authz seam (closes #2's open point):** absorbed `authz.ListRelations(ctx, user, object, relations)` hides raw `openfgav1` BatchCheck; the generated `RelationLister` stays in generated output; `authz.Authorizer` satisfies it. 7. **Read model:** per-board KV buckets carry resource state; actions recompute per render against live FGA — permission changes reflect on the next push, no refresh. 8. **Grown model:** board relations split per action (can_delete_board, can_add_column, …, can_archive_card, can_restore_card, …), all `[user]`-typed, seeded so granularity is visible (bob = member without can_delete_board). **Plan: `docs/plans/hateoas-actions.md`.** Implementation deferred to the post-map clean rewrite per the map's deliverable shape. OpenAPI-driven routes left as map fog.
Author
Owner

Design settled: can_ convention, actions codegen from FGA model, permission-only; plan docs/plans/hateoas-actions.md. State verification graduated to #24.

Design settled: can_ convention, actions codegen from FGA model, permission-only; plan docs/plans/hateoas-actions.md. State verification graduated to #24.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ginjiruu/toolbelt#11
No description provided.