user-permission-editorrun.md01_define/output/spec.mdAuthorisation now resolves through the permission engine (permission-foundation): a user's effective
set is template(role) ∪ grants − revocations, and the UserPermissionOverride model already stores
per-user grants, revocations, and an append-only audit trail. But that per-user layer ships dark —
there is no management surface. An admin cannot hand one specific expert invoice.view, or strip
lead.export from one CSM, without editing the whole role template and affecting everyone in that
role. This is the core promise of the platform-access-permissions epic (Build the Bridge / 2026-Q2
Objective 3 — validating the technical infrastructure): fine-grained, per-user control that the
role-template editor deliberately cannot give. The data model and resolver exist; this run gives them
a UI.
Add a per-user Permissions tab to the existing user directory detail page (/users/[id]),
reusing the role-template editor's feature×action grid so the two surfaces read identically:
template (inherited from the role), granted (added by a per-user override), or revoked
(stripped by a per-user override) — plus the scope qualifier (own / full) on granted cells.UserPermissionOverride delta through a new
service/action: granting a template-absent permission adds a grant; revoking a template-granted
permission adds a revocation; returning a cell to its template state clears the prior override. Every
change appends an audit entry (permission, change, who, when) to the override document.resolveActionContext({ allowedRoles: ["admin"] }) on the actions, an admin-only route policy on
the tab surface), even though the user directory itself admits admin/csm/sdm. CSM/SDM viewing a
user profile do not see the Permissions tab.clear audit entry.template / granted / revoked) and the scope
qualifier on granted cells.admin; csm/sdm on the same user-directory
profile page do not see it, and the underlying actions reject non-admin callers.admin-view-as emulation (epic
stub 9), which is not built yet; the per-row entry point lands when that emulation does.tenantPlugin).02_build/output/notes.mdpackages/services/src/db/services/user-permission-override/{index,instance}.ts: new
UserPermissionOverrideService. getUserPermissionMatrix(tenantId, userId) resolves one user's
effective matrix over every registry permission with per-cell provenance
(template/granted/revoked/none) + scope, and returns the override audit history with actor
names resolved. setUserPermissions(...) derives override deltas from the desired-effective set vs
the role template (a template-absent or scope-differing permission → grant; a dropped template
permission → revocation), diffs against the stored override into grant/revoke/clear audit
entries, and upserts (bounded audit $slice). Admin target is rejected in both methods (superuser).packages/services/src/db/services/index.ts: export the new service + its types (flows to
@sustentus/services/server via the db barrel).apps/web/app/(app)/users/[id]/permissions-actions.ts: getUserPermissionMatrix and
setUserPermissions server actions, both gated allowedRoles: ["admin"] (mirrors the
role-template editor; the registry models workspace features, not this management surface, so
there is no requiredPermission to derive from). Validates input, revalidates the profile path.apps/web/components/users/user-profile/user-permissions.tsx: admin-only server component. Builds
the feature×action groups from the registry (same humanise idiom as the role-templates page) and
renders the panel; returns null when the gated action fails (non-admin) so the surface is hidden.apps/web/components/users/user-profile/user-permissions-panel.tsx: client grid reusing the
role-template editor's checkbox + scope-select cell pattern, with per-cell provenance badges
(Granted / Revoked), a button tablist switching the matrix ↔ audit view, dirty-tracked save, and
router.refresh() after save so provenance + audit reflect the new state. Admin target renders
read-only all-granted with a note.apps/web/components/users/user-profile/user-profile.tsx: render <UserPermissions> next to
<ManageRoles> on the directory detail page.setUserPermissions writes a grant; the resolver reads
the same override doc, so the user holds it next request; other users of the role are untouched
(per-user override only).clear audit entry.getUserPermissionMatrix, badges + scope select in the panel.admin; the
surface is hidden for csm/sdm and the actions reject non-admin callers.allowedRoles: ["admin"] (no registry requiredPermission) — the
Define-stage decision: the registry models workspace features, not management surfaces, mirroring
the sibling role-template editor.admin-view-as, stub 9) per the spec.03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-07-17-user-permission-editor/page.mdx · personas: admin · PR #665
You can now grant or revoke individual permissions for one person from the user directory, without changing what everyone else in their role can do.
03_release/output/investor-update.mdWho it's for: Platform admins What shipped: A per-user Permissions tab in the user directory to grant or revoke individual permissions, each cell showing where it comes from, with a full audit trail. Why it matters: Precise, exception-level access control without touching whole roles — advancing Build the Bridge's Objective 3, Validate Technical Infrastructure & Payout Flow.
Dig deeper: <merged-PR URL> · <changelog entry URL>
03_release/output/release.mdUserPermissionOverride model + effective-permissions resolver are already documented in apps/docs/app/technical/packages/services (permission-foundation); this run adds only an admin UI + a thin management service below that page's module-level granularityapps/docs/app/business/roles/page.mdx — added a per-user-permissions capability bullet to the admin section, alongside role templates / extra roles / view-asapps/help/app/changelog/2026-07-17-user-permission-editor/page.mdx) + investor draft in this PRhumanise/featureGroups in user-permissions.tsx duplicate ~15 lines from the
role-templates settings page — accepted (deliberate decoupling; the page helper can't import the
service, and the two surfaces evolve independently).UserPermissionOverrideService.resolveAudit queries User.find({_id:{$in}}) without an
explicit tenantId — accepted (correct by globally-unique _id; actors are same-tenant admins).setUserPermissions writes a grant; resolver reads the same override
doc, so the user holds it next request; other role members unaffected (per-user override only).clear audit entry.getUserPermissionMatrix, badges + scope select in the panel.admin; hidden for
csm/sdm and the actions reject non-admin callers.