Observation-class finding recorded during the #7331 browser verification run. Filed unassigned, no pm:queue — for triage to grade. It is not a routing regression from #7213; the routing half was verified correct and is stated below so the next reader does not re-derive it.
What happens
A non-admin who is a pending approver opens the Approvals Inbox, sees the request, and clicks the record chip in its row (INV-1002). The record page renders "Record not found — The record you are looking for does not exist or may have been deleted."
The record exists and the request is live.
The routing is correct — that part was ruled out first
The rendered href is exactly what #7213 intended: it carries the current app's segment, the right object and the right record id.
INV-1002 -> /_console/apps/com.objectstack.account/showcase_invoice/record/9oAya50sd_E-PEWH
Loading the same URL shape under the same app with a record the persona can read renders the record page normally (verified with showcase_project/record/1YtAy4jz7Wsi9m9k under com.objectstack.account). So the account app can host a record page for a foreign business object; nothing about the app segment is wrong.
The real cause: record-level visibility
The authoritative server answer for that persona is a 404, not a 403:
GET /api/v1/data/showcase_invoice/9oAya50sd_E-PEWH
404 {"error":"Record 9oAya50sd_E-PEWH not found in showcase_invoice","code":"RECORD_NOT_FOUND"}
The persona holds contributor, whose showcase_invoice grant is owner-scoped; the invoice is owned by another user, so it is filtered out of the row set before the by-id read. The same read as admin returns the record. Transferring owner_id to the persona did not change the outcome either (the seeded showcase rows carry organization_id: null), so the showcase fixture makes this easy to hit — but the general shape is not showcase-specific: approver resolution routes on positions, record visibility is a separate gate, and nothing reconciles the two.
Why it is worth recording rather than shrugging at
The drawer degrades well: it carries the request's payload snapshot (record title, object, subtotal, account, status), so an approver can decide without opening the record, and the decision itself works — the run approved this exact request from the drawer successfully. So this is not a blocked approval.
What is unsatisfying is the affordance: the inbox offers a record link that, for the person it is offered to, leads to a page saying the record may have been deleted. The message is also the least useful of the available truths — the honest one is "you do not have access to this record", which the platform renders elsewhere (sys_inbox_message renders exactly that for the same persona).
Candidate dispositions (not decided here)
- Leave it. Approvals decide from the snapshot by design; record access is the security model's business. Cheapest, and defensible.
- Suppress the record link in an inbox row when the viewer cannot read the target — needs a per-row readability probe the inbox does not do today.
- Keep the link and fix only the landing message: distinguish "not visible to you" from "does not exist" on the record page. Smallest user-facing improvement, no security change.
- Grant an approver implicit read on the record under approval for the life of the request. The largest surface by far — it makes approver routing a source of record visibility, which is a real security decision and should not be taken as a UX fix.
No option recommended; 4 in particular is a security-model change, not a polish item.
Evidence
Measured 2026-08-10, examples/app-showcase, framework 88154bee1, console dist stamp 8aad9fd50b16750aebdd294392ff79540f17cd32. Non-admin persona holding contributor + finance. Screenshots of the "Record not found" landing, of the same URL shape rendering a readable record, and the paired API reads were captured in the run.
Generated by Claude Code
Observation-class finding recorded during the #7331 browser verification run. Filed unassigned, no
pm:queue— for triage to grade. It is not a routing regression from #7213; the routing half was verified correct and is stated below so the next reader does not re-derive it.What happens
A non-admin who is a pending approver opens the Approvals Inbox, sees the request, and clicks the record chip in its row (
INV-1002). The record page renders "Record not found — The record you are looking for does not exist or may have been deleted."The record exists and the request is live.
The routing is correct — that part was ruled out first
The rendered href is exactly what #7213 intended: it carries the current app's segment, the right object and the right record id.
Loading the same URL shape under the same app with a record the persona can read renders the record page normally (verified with
showcase_project/record/1YtAy4jz7Wsi9m9kundercom.objectstack.account). So the account app can host a record page for a foreign business object; nothing about the app segment is wrong.The real cause: record-level visibility
The authoritative server answer for that persona is a 404, not a 403:
The persona holds
contributor, whoseshowcase_invoicegrant is owner-scoped; the invoice is owned by another user, so it is filtered out of the row set before the by-id read. The same read as admin returns the record. Transferringowner_idto the persona did not change the outcome either (the seeded showcase rows carryorganization_id: null), so the showcase fixture makes this easy to hit — but the general shape is not showcase-specific: approver resolution routes on positions, record visibility is a separate gate, and nothing reconciles the two.Why it is worth recording rather than shrugging at
The drawer degrades well: it carries the request's payload snapshot (record title, object, subtotal, account, status), so an approver can decide without opening the record, and the decision itself works — the run approved this exact request from the drawer successfully. So this is not a blocked approval.
What is unsatisfying is the affordance: the inbox offers a record link that, for the person it is offered to, leads to a page saying the record may have been deleted. The message is also the least useful of the available truths — the honest one is "you do not have access to this record", which the platform renders elsewhere (
sys_inbox_messagerenders exactly that for the same persona).Candidate dispositions (not decided here)
No option recommended; 4 in particular is a security-model change, not a polish item.
Evidence
Measured 2026-08-10,
examples/app-showcase, framework88154bee1, console dist stamp8aad9fd50b16750aebdd294392ff79540f17cd32. Non-admin persona holdingcontributor+finance. Screenshots of the "Record not found" landing, of the same URL shape rendering a readable record, and the paired API reads were captured in the run.Generated by Claude Code