role-template-editorrun.md01_define/output/spec.mdPer-tenant role templates (RoleTemplate, shipped by permission-foundation) define what each
persona can do by default, and the enforcement cutover is now live on main — server actions, route
policies/handlers, and the workspace UI all read the resolved effective permission set. But the
templates themselves are still only editable by developers via a seeding migration: "change what all
CSMs can do" is a code change and a deploy, not a settings edit. This blocks the settled product
decision (findings §5.4, §7.2) that admins manage role defaults in-app, and it advances
2026-Q2 Objective 3 (Validate Technical Infrastructure & Payout Flow) by putting the last piece of
the access-and-permissions system — self-serve role administration — in owners' hands.
An admin-only Role templates settings page in apps/web:
FEATURES / ALL_PERMISSIONS), each cell a permission toggle with a
scope qualifier (own / full) shown when granted. The current per-tenant template state is
pre-filled; a "customised vs platform default" indicator uses the existing isPlatformDefault flag.RoleTemplate and, because enforcement is live, affected users'
effective permissions change on their next request (the resolver reads the template first). Each
save flips isPlatformDefault to false, stamps updatedBy, and appends an audit entry (who,
when, which permission, what change) to an append-only, bounded audit trail on the template —
mirroring the audit array already on UserPermissionOverride.admin role is a hard superuser: its grid renders full-granted and read-only, not editable
by construction (decision §7.2). The resolver already short-circuits admin to allow-all, so the
read-only grid is a faithful, lockout-proof view — an admin can never strip their own access here.PLATFORM_ROLE_TEMPLATE_DEFAULTS, sets isPlatformDefault back to true, and writes an audit entry.RoleTemplate is updated and affected users' resolved permissions change on their
next request.admin role's template renders as all-granted and cannot be edited or reset — the save/reset
controls are absent/disabled for it, so lockout from the permission system is impossible.user-permission-editor (depends on this feature).in-app-role-assignment.user-permission-editor's concern.main, so an edit takes real effect on the user's next request (no
shadow-mode framing needed); the audit trail follows the existing UserPermissionOverride pattern;
admin read-only is decided (§7.2). Nothing here blocks Build.02_build/output/notes.mdThe stub carried a caveat: if the enforcement cutovers hadn't shipped, template edits would only feed
shadow-mode logs, so the UI would need to say so. The owner confirmed at Build start that enforcement
is now merged on main (routes-permission-cutover, #656, plus the actions + UI cutovers). The branch
was rebased onto that main, so an edit here takes real effect on a user's next request and no
shadow-mode framing is needed — the "Assume enforcement live" copy path.
packages/services — model + servicedb/models/role-template.ts — added an append-only, bounded (MAX_AUDIT_ENTRIES = 500) audit
array mirroring the one already on UserPermissionOverride. Entry = { permission?, scope?, change, by, at } where change ∈ grant | revoke | scope | reset (permission/scope absent on a
whole-template reset). Backward compatible — existing seeded docs read fine and $push creates the
array on first write, so no migration is required.db/services/role-template/ — new RoleTemplateService (+ instance.ts), exported from
db/services/index.ts:listForTenant(tenantId) → one RoleTemplateView per role in USER_ROLES order, falling back to
the platform default when a tenant has no stored row. admin is always returned full-granted and
isReadOnly.updateRolePermissions(tenantId, role, desired, byAppUserId) → sanitises the desired grid against
the registry (isPermission), diffs it against the stored template into per-change audit entries,
flips isPlatformDefault to false, stamps updatedBy, and upserts. Returns the change count (0 =
no-op). Rejects role === "admin".resetRoleToDefault(tenantId, role, byAppUserId) → restores the platform default, flips
isPlatformDefault back to true, writes a single reset audit entry. Rejects role === "admin".apps/web — settings page, actions, wiringapp/(app)/admin/settings/role-templates/page.tsx — admin-gated server component
(resolveActionContext({ allowedRoles: ["admin"] })). Builds the serialisable feature × action grid
from the /server registry (ALL_FEATURES/FEATURES) and each role's current + platform-default
scope maps, and passes them to the client editor — keeping @sustentus/services/server out of the
client bundle._components/role-templates-editor.tsx — client editor. Role selector; per role a feature-grouped
grid of permission checkboxes each with an own/full scope Select when granted; a
"Platform default"/"Customised" badge; Save (dirty-gated) and Reset-to-defaults. The admin role
renders full-granted, read-only, with no Save/Reset — lockout-proof by construction.actions.ts — saveRoleTemplate / resetRoleTemplate server actions: Zod-parse (role restricted
to non-admin EDITABLE_ROLES), admin-gate, call the service, revalidatePath. Not in the
migratedActionFiles glob, so the require-action-permission rule does not apply — this admin/config
surface gates on allowedRoles: ["admin"] like the other settings actions._components/tabs-config.ts — new admin-only "Role templates" settings tab.lib/route-policies.ts — explicit admin-only rule for /admin/settings/role-templates (the
/admin/settings admin-only rule already covered it; the explicit entry documents intent and is
robust to future broadening).RoleTemplate is updated and the resolver (which reads it first) changes effective access on the
next request.admin role renders all-granted and read-only — no Save/Reset controls; service + action + a
route/tab that is admin-only all reject an admin-role edit — lockout is impossible.by,
at) to the template's bounded audit trail.tenantId filter + tenant plugin); no cross-tenant template
is visible or writable.Local pnpm build/lint/typecheck is owned by the factory (blocked locally by the repo hook) — verified
by reading back the PR's Vercel preview + CI check runs after push.
03_release/output/changelog.mdYou can now change the default permissions for each role in your organisation from Settings → Role templates — no developer or deploy needed.
03_release/output/investor-update.mdWho it's for: Admins (and every role whose access they set). What shipped: A Role templates settings page where an admin edits each role's default permissions in-app; changes are audited and apply on the user's next request. Why it matters: Access management moves from code to a self-serve settings screen, completing the permission system behind the payout flow.
Advances Build the Bridge — Objective 3: validate technical infrastructure & payout flow.
Dig deeper: https://github.com/sustentus/sustentus/pull/659
03_release/output/release.mdapps/docs/app/technical/packages/services/page.mdx — RoleTemplate now carries an audit trail; templates are admin-editable in-app (roleTemplateService, Settings → Role templates)apps/docs/app/business/roles/page.mdx — admin persona gains the Role templates settings tab + capability. platform-overview/feature-role-matrix: no impact (admin config surface, not a service-journey feature)apps/help/app/changelog/2026-07-16-role-template-editor/page.mdx + investor draft in this PRRan /code-review high (complexity: complex) on the full diff (code + docs + changelog). Two findings, both fixed on the branch before merge:
handleSave set isPlatformDefault(false) even when the server reported changes === 0, diverging the UI badge from the persisted flag. Fixed: only flip when changes > 0.humanise() title-cased product acronyms to "Brd"/"Csat". Fixed with a small acronym map → "BRD"/"CSAT".Earlier, the Vercel agent review flagged that switching role tabs unmounted the panel and discarded unsaved edits; fixed in commit 1415342 (panels stay mounted via hidden), and the bot marked it ISSUE_RESOLVED.
RoleTemplate is updated and the resolver (which reads it first) changes effective access on the next request — updateRolePermissions upserts the template; enforcement is live on main.admin role renders all-granted and read-only (no Save/Reset); service + action + admin-only route/tab all reject an admin-role edit — lockout impossible.by, at) to the template's bounded audit trail.tenantId filter + tenant plugin); no cross-tenant template is visible or writable.