`board:configure` — the boards, their columns, swimlanes and WIP limits (and NOT the card drag, which stays `work_item:edit`)
Board CONFIGURATION — creating a board, adding or deleting a column, swimlanes, WIP limits — is administrative and is board:configure.
⚠️ Re-scoped 2026-08-06 against the branch — this card's original premise was BACKWARDS
The card was written from the guard's output and claimed "four of its operations reach boardsService with no project gate at all today… any signed-in workspace member can add or delete a column on any board they can reach. Closing that is most of this card's value." That is false, and the opposite is true. Every board-shape write in lib/services/boardsService.ts — createBoard, renameBoard, setDefaultBoard, deleteBoard, addColumn, renameColumn, reorderColumn, deleteColumn, setColumnWipLimit, setSwimlaneGroupBy — already calls the module-private assertBoardConfigAdmin (boardsService.ts:1138), which resolves isOwnerRole(workspaceMembership.role) and throws NotBoardAdminError ("You must be a workspace owner to change board configuration.").
The guard could not see it because its GATE pattern is a whitelist of NAMES and assertBoardConfigAdmin is not one of them — MOTIR-2304 repairs the instrument and this card is blocked_by it, so the enumeration below is run against a walk that can see the gate.
So this card LOOSENS, it does not close a hole. Board configuration today is workspace-OWNER-only — tighter than project:administer. After the split it is board:configure, which a workspace admin and a project admin also hold. That is a real grant to real actors and it is named here rather than smuggled in behind a refactor.
It is also required, not optional. MOTIR-2293 already put board:configure into BUILTIN_ROLE_PERMISSIONS.admin. Leaving the gate at workspace-owner-only would have the catalog advertise a permission a project admin holds while the code refuses them — the exact lie lib/permissions/catalog.ts's opening rule exists to prevent. Both mirrors agree: Jira team-managed and Plane both put board configuration under project administration, not under the org owner.
⚠️ The inventory's board:configure mapping is too wide, and following it literally would break the product
Under reason R9 it also assigns the key to:
/api/boardGET — the board PROJECTION read (assertCanBrowsetoday). Staysproject:browse./api/boardPATCH — resolves tosetSwimlaneGroupBy/renameBoard, which ARE board configuration. It ISboard:configure; the row must be split by verb because the two halves land on different keys./api/board/move— dragging a card between columns.assertCanEdittoday, and the single most-used write in the product. An administrative key here would make the board read-only for every member and viewer in every project. It stayswork_item:edit.
Correct those rows as part of this card. A mapping that would move an operation into the wrong domain is a defect in the map, not an instruction.
Acceptance criteria
- Enumerate first, against the REPAIRED guard (MOTIR-2304 is
blocked_by): list itsboard:configureoperations, then open each row in the inventory'sboardsection. The PR body carries the resulting table — operation, gate today (the real one, namingassertBoardConfigAdminwhere it applies), gate after — with a one-line reason for each row that does NOT move toboard:configure. - Every board/column/swimlane/WIP write in
lib/services/boardsService.tsassertsboard:configurethroughprojectAccessService.assertPermission, threadingtxwhere the method already runs inside one.assertBoardConfigAdminis DELETED — it is the third private re-implementation of the access policy this story has found, and leaving it would keep a second answer alive next to the first. NotBoardAdminErrorand itsNOT_BOARD_ADMINwire code:git grep -n "NOT_BOARD_ADMIN\|NotBoardAdminError" -- app lib components tests e2eFIRST and state in the PR what each consumer receives after. Its message ("You must be a workspace owner…") is false once a project admin may configure a board, so either the error is retired in favour ofPermissionDeniedErroror its copy is corrected — say which, and keep every consumer compiling.- The WIDENING is asserted, not just performed. A test proves the new actor set explicitly: a project admin can now add a column (they could not before this card); a workspace admin can too; a project member and viewer are refused 403; a non-browser gets 404. The project-admin case is the one that changes, so it is the one that must be named.
/api/boardGET stays on the browse gate./api/board/moveis untouched — a diff that changes its gate fails this card.- A member can still MOVE a card and still READ the board, asserted explicitly — the regression this card is most likely to cause.
boardsService.mapStatusToColumnis NOT touched here — the column→status mapping isworkflow:manageand belongs to the workflow card, which edits the same file. Keep this PR to the board-shape methods so the two do not collide beyond a trivial rebase.board:configureflips toenforcement: 'enforced'inlib/permissions/catalog.ts, and is added to the wired listtests/permissions/catalog.test.tspins.- The inventory's
boardrows carry the corrected gate-today and gate-after, withnew→existingon the ones this card wires, and the/api/boardrow split by verb. - The guard's pending pin is re-derived by running the suite on the branch.
pnpm testgreen;pnpm lintand the prettier check pass repo-wide.
Context refs
lib/services/boardsService.ts—assertBoardConfigAdmin(line ~1138),addColumn, the board CRUD, swimlane and WIP methods, andmapStatusToColumn(the workflow card's, not this one's).lib/boards/errors.ts—NotBoardAdminErrorand its copy.app/api/boards/route.ts·app/api/boards/[id]/route.ts·app/api/board/columns/route.ts·app/api/board/columns/[columnId]/route.ts— the four the card called ungated; they are gated, through the service.app/api/board/route.ts·app/api/board/move/route.ts— the two the inventory over-claims; read them, do not re-point them blindly.docs/decisions/permission-inventory.md— theboardsection and reason R9.- The instrument repair this waits on · the seam this calls · the workflow card that shares
boardsService.