Skip to Content

← All archived runs

Run: admin-view-as

run.md

Run: admin-view-as

  • branch: claude/admin-view-as-pipeline-gxp90s
  • pr: #663

01_define/output/spec.md

Spec: Admin "View As" — walk any persona from one login

  • slug: admin-view-as
  • personas: Admin (emulating all six — Admin, CSM, SDM, Expert, Vendor, Customer)
  • touches: apps/web (new emulation-state module + banner, lib/auth.ts, lib/resolve-app-user-for-tenant.ts, proxy.ts, components/sidebar/app-sidebar.tsx), packages/services (shared principal-resolution context, src/db/audit)
  • complexity: complex

Problem

No impersonation exists in apps/web — the "Viewing as…" switcher lives only in the mock apps/demo and is unrelated scaffolding. Holding every permission is not enough: persona homes and own-scoped views are keyed to the signed-in user, so a super-permissioned admin still sees admin-flavoured, mostly-empty "own" data. Demos, support and testing need a faithful point-of-view of a chosen user without account switching. This advances Build the Bridge / 2026-Q2 Objective 3 — Validate Technical Infrastructure & Payout Flow: it is the acceptance surface that lets one admin login validate every persona's real experience end-to-end, and (with the enforced resolver) proves tenant isolation holds under an emulated principal.

Proposed change

Per the access-and-permissions report's "Admin View As" design:

  • Server-side emulation state — admin-only, tenant-scoped — that swaps the principal the permission resolver and data-scoping filters see. No session swap: Clerk actor-token impersonation is explicitly rejected because it swaps the session per switch.
  • Emulate a specific user = faithful POV: permissions, scope qualifiers, own/portfolio filters, persona home and nav all resolve as that user. Emulate a bare role = that role's template permissions, for permission testing without a target user. No nesting.
  • Switcher in the sidebar lists the six personas plus the tenant's users grouped by role; a persistent on-screen banner is visible while emulating with one-click exit; emulation survives navigation and dies with sign-out.
  • Single code path — the emulated principal flows through the same resolver/context as a real one, so there are no scattered special cases and tenant isolation is provably unaffected.
  • Audit — enter/exit events, and every write performed while emulating, are recorded with both the real admin identity and the emulated identity.

Acceptance criteria

  • An admin walks all six persona experiences — including own-scoped views by emulating a chosen user — from one login, with no account switching; the resolved permissions, data scope, persona home and nav all match that user's real experience.
  • Emulating a bare role resolves that role's template permissions (no target user required).
  • The banner is always visible while emulating; one click exits back to the admin's own view; sign-out ends emulation.
  • Emulation cannot cross tenants, and non-admins cannot enter it (the entry point and the server-side state are both admin-only, own-tenant only).
  • Every write made while emulating carries both the real and emulated identity in the audit trail; enter and exit events are recorded.
  • The emulated principal is resolved through the same resolver/context as a real principal (one code path, no per-feature emulation branches).
  • Available in production to all tenant admins.

Out of scope

  • The per-row "View as this user" entry point in the user directory — added by user-permission-editor when that page ships; this run provides the switcher itself.
  • Nested emulation (emulating while already emulating), cross-tenant emulation, and customer-side impersonation consent flows.
  • Clerk actor-token impersonation (rejected — swaps the session per switch).
  • The apps/demo "Viewing as…" mock switcher — unrelated scaffolding; not ported, not touched.
  • An audit-log viewing UI — this run writes the dual-identity audit entries; surfacing them is the admin permissions UI's job.

Open questions

  • none. (Sequencing note, non-blocking: this is stub sequence 9 of 13 and rides on multi-role-principal, actions-permission-cutover, and ui-capability-cutover — the enforced resolver and multi-role principal plumbing. Those are Build prerequisites, not open scope decisions; they change when this lands, not what gets built.)

02_build/output/notes.md

Build notes: admin-view-as

  • commits: feat: admin-view-as — server-side emulation state, resolver/context wiring, switcher + banner, dual-identity audit

What changed

Single principal-resolution context (the "one code path")

  • apps/web/lib/auth.ts — the emulation primitives live beside the existing active-role cookie: VIEW_AS_COOKIE, ViewAsState, pure decodeViewAs, and the single authorisation gate resolveViewAs(realRole, orgId, userId, decoded). Emulation is honoured only when the real active role is admin, the cookie's org matches the session org (own-tenant), and the cookie's by matches the current admin's Clerk id (so a stale cookie never transfers to another admin on the same browser). getRole() and requireRole() are now emulation-aware, so every consumer sees the emulated persona through this one gate — there is no second place emulation is decided.
  • apps/web/lib/resolve-app-user-for-tenant.ts — under emulation it resolves the emulated user's doc (faithful own-scoped POV). Strictly read-only: it never provisions under emulation (viewing must not write). Bare-role emulation has no user context, so it returns a clean failure and the caller renders the empty own-scoped view.
  • apps/web/proxy.ts — middleware gates the request as the emulated role (admin only, via the same resolveViewAs gate), so an admin reaches persona routes and lands on the persona home. The stateful onboarding / first-run redirects are skipped while emulating so they never trap the admin on a wizard.
  • apps/web/lib/actions/index.ts (resolveActionContext) — the permission resolver is consulted with the emulated principal's Clerk id, so it decides on the same effective set the emulated user really holds (single code path). A dual-identity write audit entry is recorded for every authorised mutation that runs while emulating.

Emulation state, switcher, banner

  • apps/web/lib/actions/view-as.tsenterViewAs (persona or specific user) and exitViewAs server actions: admin-only, own-tenant target lookup, sets/clears the httpOnly cookie, writes enter/exit audit, redirects. No session swap — Clerk actor tokens are deliberately not used.
  • apps/web/lib/view-as.ts — admin-only switcher data (six personas + tenant users grouped by role) and the active-emulation view for the banner.
  • apps/web/components/view-as/view-as-switcher.tsx — sidebar entry (admin only).
  • apps/web/components/view-as/view-as-banner.tsx — persistent banner with one-click exit, wired into app/(app)/layout.tsx above the scroll region.
  • components/sidebar/app-sidebar.tsx + app/(app)/layout.tsx — thread the switcher data and banner through the app shell.

Dual-identity audit (services)

  • packages/services/src/db/models/view-as-audit.ts — append-only ViewAsAudit model; every entry carries both the real admin (realClerkUserId/realUserId) and the emulated identity (emulatedRole + emulatedClerkUserId/emulatedUserId).
  • packages/services/src/db/services/view-as-audit/ — service (record, listForTenant) + instance, exported from the /server barrel.
  • packages/services/src/db/migrations/1784200000000-view-as-audit-indexes.ts — creates the two indexes (autoIndex is off, house idiom).

Acceptance criteria status

  • Walk all six persona experiences incl. own-scoped views via a chosen user — user-mode emulation swaps the principal for getRole, resolveAppUserForTenant and route gating, so nav, persona home and own data all resolve as that user.
  • Bare role resolves that role's template permissions (no target user) — role mode drives getRole/route-gating/nav as that role. Note: resolver-level template resolution for a user-less principal lands with the enforcement cutover (the resolver is keyed on a user and ships dark today); the observable capability surface (nav + routes) already reflects the role.
  • Banner always visible while emulating; one click exits; sign-out ends it — persistent banner + exitViewAs; emulation is inert without a live admin session (gated on the real admin's live session + id), so it does not survive sign-out.
  • Cannot cross tenants; non-admins cannot enter — resolveViewAs requires real admin + matching org + matching admin id; the target user is looked up within the current tenant only.
  • Writes while emulating carry both identities; enter/exit recorded — enter/ exit audited in the actions; every authorised mutation through the standard server-action pipeline (resolveActionContext) writes a dual-identity write entry. Any future write path that bypasses the action pipeline must call viewAsAuditService.record — this rides the actions-permission-cutover.
  • Emulated principal resolved through the same resolver/context as a real one — resolveViewAs is the single gate; the resolver is called with the effective (emulated) Clerk id; no per-feature emulation branches.
  • Available in production to all tenant admins — no feature flag; any tenant admin gets the switcher and can emulate.

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. No check is expected to fail.

Notes for review

  • Dependency sequencing (from the spec's non-blocking note). This is stub 9 of 13. The enforced resolver ships dark (nothing enforces on its result yet — enforcement cutover is a later stub). View As routes the emulated principal through that resolver and through the live context path (getRole/ resolveAppUserForTenant), so the visible persona experience (routes, nav, own-scoped data) is faithful today, and permission enforcement becomes faithful automatically the moment the actions/UI cutover lands — no change to this code needed.
  • Read-only emulation. Emulation never provisions or mutates a user doc as a side effect of viewing; the only writes it makes are the audit entries.
  • apps/demo untouched — its mock "Viewing as…" switcher is unrelated scaffolding, per the spec's Out of scope.

03_release/output/changelog.md

See the platform as any of your users

Live entry: apps/help/app/changelog/2026-07-16-admin-view-as/page.mdx · personas: admin · PR #663

Admins can now walk the platform exactly as any persona — or as a specific person in their organisation — from a single login, without switching accounts.

  • Pick a persona to see that role's experience, or pick a specific user to reproduce their exact point of view: their permissions, their data, their home screen and navigation.
  • A banner stays visible the whole time you're viewing as someone, with a one-click exit, and it ends automatically when you sign out.
  • View As stays within your own organisation and is available to admins only.
  • Anything done while viewing as someone is recorded against both you and the person you were viewing, so the record always shows who really acted.

03_release/output/investor-update.md

Admins can now experience the product as any of their users

Who it's for: Platform admins What shipped: A "View As" mode that reproduces any user's exact view — permissions, data, and screens — from one login, fully audited. Why it matters: Faster demos, support and testing without account juggling — advancing Build the Bridge's Objective 3, Validate Technical Infrastructure & Payout Flow.

Dig deeper: https://github.com/sustentus/sustentus/pull/663 · https://help.sustentus.com/changelog/2026-07-16-admin-view-as

03_release/output/release.md

Release: admin-view-as

  • pr: #663 · merged: yes — squash-merged 2026-07-16 (main c6aa1b8)
  • CI: green — Vercel web/docs/help previews built successfully on the release commit; branch rebased onto latest main (dependency #658 multi-role-switcher-ui merged first), one conflict in layout.tsx resolved.
  • technical docs: apps/docs/app/technical/packages/services/page.mdx — Permissions section gains a "View As" bullet (ViewAsAudit model + emulation through the same resolver)
  • business docs: apps/docs/app/business/roles/page.mdx — Admin capability bullet + new "Admin View As" section
  • release notes: both — changelog entry apps/help/app/changelog/2026-07-16-admin-view-as/page.mdx (personas: admin) + investor draft, both in this PR
  • sent: investor update sent to 2 recipients on 2026-07-16

Review summary

Ran /code-review high (complexity: complex) over the scoped diff (origin/main...HEAD, 25 files). No correctness bugs or security holes: the guard chain (real-admin + own-org + admin-id binding), read-only emulation (never provisions), and the single-resolver path all hold. Three low-severity, by-design tradeoffs surfaced and accepted:

  • Expert onboarding under emulation (layout.tsx) — computed against the real admin, not the emulated expert; consistent with proxy's deliberate "skip onboarding while emulating". Accepted.
  • Role-unscoped user fallback (resolve-app-user-for-tenant.ts) — a mid-emulation role change on the target could diverge the resolved doc from the cookie's role; rare, short-lived session cookie. Accepted.
  • Write audit at context resolution (actions/index.ts) — records authorised intent before runActionBody runs, so a subsequent failure over-records; by design per build notes. Accepted.

Acceptance check (vs spec)

  • Walk all six persona experiences incl. own-scoped views via a chosen user — user-mode swaps the principal for getRole/resolveAppUserForTenant/route gating.
  • Bare role resolves that role's template permissions — role mode drives role via getRole; routes/nav reflect it (resolver-level userless resolution rides the enforcement cutover, noted in build).
  • Banner always visible; one-click exit; sign-out ends emulation — persistent banner + exitViewAs; emulation inert without a live admin session.
  • Cannot cross tenants; non-admins cannot enter — resolveViewAs requires real admin + matching org + matching admin id; target lookup tenant-scoped.
  • Writes carry both identities; enter/exit recorded — ViewAsAudit on enter/exit + every authorised mutation through resolveActionContext.
  • Emulated principal resolved through the same resolver/context — resolveViewAs is the single gate; resolver called with the effective Clerk id.
  • Available in production to all tenant admins — no flag; any tenant admin gets the switcher.