Measured on objectui at the current checkout, against a live seeded app: a read-only "member detail" record page (objectstack-ai/duly#13) rendered 8 "+ New" buttons it had no way to decline.
The gap
PageComponentSchema has no readonly / disabled, and that is a deliberate boundary — ruled 2026-08-12, "editability lives on fields", with EDITABILITY_BOUNDARY_KEYS redirecting an author who writes one. The prescription is right for a field.
It does not reach a related list's CRUD affordances, because those are not fields. RelatedRecordActionsBridge resolves them from the child object:
constrawAff=resolveEffectiveCrudAffordances(childDef,getObjectApiOperations(objectName));constobjectCanCreate=rawAff.create&&can(objectName,'create');
— the child's userActions bucket, intersected with API operations and the principal's grant. Nothing on the page participates. RecordRelatedListProps can decline the opt-in Add-existing picker (add) and row actions, but "+ New", row edit and row delete arrive from the host regardless.
So for a child object that any user may create — here duly_task, where every position holds allowCreate because people do create their own tasks elsewhere in the app — a page whose entire premise is "the manager enters nothing" is handed a create affordance on every task list on it.
The only lever available is object-level (duly_task.userActions.create), which would close creation everywhere — including the screens where it is correct. That is not a workaround, it is a different and wrong change.
Why it is worth a key rather than a doc note
The affordance is not merely cosmetic on a page like this: a manager pressing "+ New" on someone else's task list creates work for that person from a screen documented as read-only, and the write is legal server-side. The page is the only layer that knows this surface is a read surface — the child object is used correctly elsewhere, and the principal's grant is correct too.
Suggested shape
A per-component switch on record:related_list — readonly: true, or affordances: 'none' — consumed where the bridge's handlers are wired in RecordRelatedListBody, narrowing only (never re-opening something the bucket/API/grant already closed, matching the posture createPredicates already takes). A page-level readonly: true that defaults every component would be even better for this use case, but the per-component key is the smaller change and composes.
Related but distinct, both closed: #4646 (related-list "+ New" not consuming userActions.create predicates) and #5153 (the object-list toolbar's half). Those made the object's verdict reach the button. This is about the page having no verdict to give.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Measured on objectui at the current checkout, against a live seeded app: a read-only "member detail" record page (objectstack-ai/duly#13) rendered 8 "+ New" buttons it had no way to decline.
The gap
PageComponentSchemahas noreadonly/disabled, and that is a deliberate boundary — ruled 2026-08-12, "editability lives on fields", withEDITABILITY_BOUNDARY_KEYSredirecting an author who writes one. The prescription is right for a field.It does not reach a related list's CRUD affordances, because those are not fields.
RelatedRecordActionsBridgeresolves them from the child object:— the child's
userActionsbucket, intersected with API operations and the principal's grant. Nothing on the page participates.RecordRelatedListPropscan decline the opt-in Add-existing picker (add) and rowactions, but "+ New", row edit and row delete arrive from the host regardless.So for a child object that any user may create — here
duly_task, where every position holdsallowCreatebecause people do create their own tasks elsewhere in the app — a page whose entire premise is "the manager enters nothing" is handed a create affordance on every task list on it.The only lever available is object-level (
duly_task.userActions.create), which would close creation everywhere — including the screens where it is correct. That is not a workaround, it is a different and wrong change.Why it is worth a key rather than a doc note
The affordance is not merely cosmetic on a page like this: a manager pressing "+ New" on someone else's task list creates work for that person from a screen documented as read-only, and the write is legal server-side. The page is the only layer that knows this surface is a read surface — the child object is used correctly elsewhere, and the principal's grant is correct too.
Suggested shape
A per-component switch on
record:related_list—readonly: true, oraffordances: 'none'— consumed where the bridge's handlers are wired inRecordRelatedListBody, narrowing only (never re-opening something the bucket/API/grant already closed, matching the posturecreatePredicatesalready takes). A page-levelreadonly: truethat defaults every component would be even better for this use case, but the per-component key is the smaller change and composes.Related but distinct, both closed: #4646 (related-list "+ New" not consuming
userActions.createpredicates) and #5153 (the object-list toolbar's half). Those made the object's verdict reach the button. This is about the page having no verdict to give.🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p