Skip to Content

← All archived runs

Run: role-template-editor

run.md

Run: role-template-editor

  • branch: claude/role-template-editor-pipeline-eaevon
  • pr: #659

01_define/output/spec.md

Spec: Role-template editor (admin settings)

  • slug: role-template-editor
  • personas: Admin
  • touches: apps/web (settings route, server actions, route policy, nav), packages/services/server (RoleTemplate audit + service functions)
  • complexity: complex

Problem

Per-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.

Proposed change

An admin-only Role templates settings page in apps/web:

  • One view per role (admin, csm, sdm, expert, vendor, customer): the feature × action grid drawn from the code registry (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.
  • Edits write the per-tenant 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.
  • The 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.
  • Reset to platform defaults per role: restores that role's template to PLATFORM_ROLE_TEMPLATE_DEFAULTS, sets isPlatformDefault back to true, and writes an audit entry.
  • Own-tenant only and admin-only at every layer: a route-policy entry plus permission checks on the server actions keep non-admin roles out (route + action enforcement).

Acceptance criteria

  • An admin can toggle a permission (and set its scope) on a non-admin role template and save; the per-tenant RoleTemplate is updated and affected users' resolved permissions change on their next request.
  • The 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.
  • Every template change (per-permission toggle, scope change, and reset) writes an audit entry capturing who, when, the permission, and the kind of change.
  • A role whose template is unmodified shows as "platform default"; after an edit it shows as "customised", and "reset to platform defaults" returns it to the platform default state.
  • Non-admin roles cannot reach the page or invoke its actions — the route redirects/denies and the server actions reject with an authorization error (route + action enforcement).
  • All reads and writes are scoped to the acting admin's tenant; no cross-tenant template is visible or writable.

Out of scope

  • Per-user permission overrides — that is user-permission-editor (depends on this feature).
  • Role assignment / changing a user's role — in-app-role-assignment.
  • Editing the feature/action registry itself (features and actions are code, deploy-fixed).
  • A full audit-history viewer UI — this feature only writes the audit trail; surfacing provenance/ history in the UI is user-permission-editor's concern.
  • Changing the resolver, enforcement layers, or scope semantics — all shipped by the foundation + cutover stubs; this feature only edits the template data they already consume.

Open questions

  • none. Enforcement is merged on 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.md

Build notes: role-template-editor

  • commits: feat: admin role-template editor — per-tenant permission grid, audit trail, reset to defaults

Context confirmed at Build start

The 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.

What changed

packages/services — model + service

  • db/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, wiring

  • app/(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.tssaveRoleTemplate / 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).

Acceptance criteria status

  • An admin can toggle a permission + scope on a non-admin role and save; the per-tenant RoleTemplate is updated and the resolver (which reads it first) changes effective access on the next request.
  • The 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.
  • Every change (toggle, scope change, reset) writes an audit entry (permission, change kind, by, at) to the template's bounded audit trail.
  • Unmodified role shows "Platform default"; after an edit "Customised"; reset returns it to the platform-default state (and flag).
  • Non-admin roles cannot reach the page (route policy) or invoke the actions (admin gate).
  • All reads/writes are tenant-scoped (tenantId filter + tenant plugin); no cross-tenant template is visible or writable.

Verify

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.md


title: Set what each role can do from settings date: 2026-07-16T12:00:00Z personas: [admin] slug: role-template-editor pr: https://github.com/sustentus/sustentus/pull/659

Set what each role can do from settings

You can now change the default permissions for each role in your organisation from Settings → Role templates — no developer or deploy needed.

  • Pick a role and turn individual permissions on or off, and choose whether each applies to a person's own records or the whole organisation.
  • Changes take effect for everyone in that role the next time they load a page.
  • A "platform default" or "customised" label shows at a glance which roles you've changed, and you can reset any role back to the platform defaults in one click.
  • The admin role always keeps every permission and can't be edited, so you can never accidentally lock yourself out.
  • Every change is recorded, so there's always a trail of who changed what and when.

03_release/output/investor-update.md

Admins can now change what each role can do — no engineering, no deploy

Who 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.md

Release: role-template-editor

  • pr: #659 (https://github.com/sustentus/sustentus/pull/659) · merged: yes — squash-merged to main (b9f5207) on 2026-07-16
  • CI: green — Vercel web deploy succeeded on the head commit; other apps skipped (not affected)
  • technical docs: apps/docs/app/technical/packages/services/page.mdx — RoleTemplate now carries an audit trail; templates are admin-editable in-app (roleTemplateService, Settings → Role templates)
  • business docs: 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)
  • release notes: both — changelog entry apps/help/app/changelog/2026-07-16-role-template-editor/page.mdx + investor draft in this PR
  • sent: investor update sent to 2 recipients on 2026-07-16

Review summary

Ran /code-review high (complexity: complex) on the full diff (code + docs + changelog). Two findings, both fixed on the branch before merge:

  • No-op save flipped the badgehandleSave set isPlatformDefault(false) even when the server reported changes === 0, diverging the UI badge from the persisted flag. Fixed: only flip when changes > 0.
  • Acronym labelshumanise() 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.

Acceptance check (vs spec)

  • An admin can toggle a permission + scope on a non-admin role and save; the per-tenant 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.
  • The 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.
  • Every change (toggle, scope change, reset) writes an audit entry (permission, change kind, by, at) to the template's bounded audit trail.
  • Unmodified role shows "Platform default"; after an edit "Customised"; reset returns to the platform-default state and flag.
  • Non-admin roles cannot reach the page (route policy) or invoke the actions (admin gate).
  • All reads/writes are tenant-scoped (tenantId filter + tenant plugin); no cross-tenant template is visible or writable.