admin-view-asrun.md01_define/output/spec.mdlib/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)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.
Per the access-and-permissions report's "Admin View As" design:
user-permission-editor when that page ships; this run provides the switcher itself.apps/demo "Viewing as…" mock switcher — unrelated scaffolding; not ported, not touched.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.mdfeat: admin-view-as — server-side emulation state, resolver/context wiring, switcher + banner, dual-identity auditapps/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.apps/web/lib/actions/view-as.ts — enterViewAs (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.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).getRole, resolveAppUserForTenant
and route gating, so nav, persona home and own data all resolve as that user.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.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.resolveViewAs requires real
admin + matching org + matching admin id; the target user is looked up within
the current tenant only.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.resolveViewAs is the single gate; the resolver is called with the effective
(emulated) Clerk id; no per-feature emulation branches.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.apps/demo untouched — its mock "Viewing as…" switcher is unrelated
scaffolding, per the spec's Out of scope.03_release/output/changelog.mdLive 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.
03_release/output/investor-update.mdWho 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.mdc6aa1b8)main (dependency #658 multi-role-switcher-ui merged first), one conflict in layout.tsx resolved.apps/docs/app/technical/packages/services/page.mdx — Permissions section gains a "View As" bullet (ViewAsAudit model + emulation through the same resolver)apps/docs/app/business/roles/page.mdx — Admin capability bullet + new "Admin View As" sectionapps/help/app/changelog/2026-07-16-admin-view-as/page.mdx (personas: admin) + investor draft, both in this PRRan /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:
layout.tsx) — computed against the real admin, not the emulated expert; consistent with proxy's deliberate "skip onboarding while emulating". Accepted.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.actions/index.ts) — records authorised intent before runActionBody runs, so a subsequent failure over-records; by design per build notes. Accepted.getRole/resolveAppUserForTenant/route gating.getRole; routes/nav reflect it (resolver-level userless resolution rides the enforcement cutover, noted in build).exitViewAs; emulation inert without a live admin session.resolveViewAs requires real admin + matching org + matching admin id; target lookup tenant-scoped.ViewAsAudit on enter/exit + every authorised mutation through resolveActionContext.resolveViewAs is the single gate; resolver called with the effective Clerk id.