demo-auto-vendor-viewrun.md02_define/output/spec.mdThe demo host is the sales surface for Q2 Objective 1 — Establish Product-Market Fit with Vendor
Partners (8+ vendors onto paid tiers), under the Build the Bridge initiative. Every prospect we
put in front of it is a vendor, and the story we sell opens on what a vendor sees: their leads,
their proposals, their revenue. Today a visitor who completes demo entry lands on /admin — the
internal operator console — and the presenter has to open the View As switcher and pick a vendor
before the demo can start. The first screen of a sales demo is the one that has to land, and right
now it is the wrong one.
Signing in as the demo admin is still correct and stays: admin is the only role that holds View As, which is what lets a presenter move between personas mid-walkthrough. Only the landing screen is wrong.
On the demo host only, once the existing entry flow has produced a real Clerk session on the demo
admin, the visitor is put into View As on the demo tenant's first vendor and lands on the vendor
dashboard (ROLE_HOME.vendor = /vendor) instead of /admin — with no click and no flash of the
admin console.
The mechanics:
/api/demo/land. DemoEntry navigates
to it (a full navigation, as today) instead of to /admin; the handler runs with the freshly
minted session cookie on the request, so it can set the View As cookie server-side and redirect
onward. The visitor sees one navigation and never renders an admin screen. It is a route handler
rather than a page because setting a cookie during a Server Component render is not permitted in
Next.groupByRole in
lib/view-as-switcher-data.ts already name-sorts users within each role, so "first" is the first
of that sorted vendor group. A new pure selector beside it returns that option (or null), and is
unit-tested in the existing view-as-switcher-data.test.ts rather than in a new harness. No new
environment variable, and therefore no turbo.json globalEnv change.enterViewAs — the admin check, the own-tenant target lookup, the cookie write and the
dual-identity audit entry — moves into a plain server module (lib/view-as-enter.ts) that both
the existing server action and the new route handler call. The action's client-callable signature
is unchanged; the audit entry and the org re-check stay on exactly one path.enter audit entry through
viewAsAuditService, carrying action: "demo_auto_enter" so the trail distinguishes it from a
presenter's manual pick. This uses the model's existing optional action field — no schema
change. The flag is set by the route handler, never by a client-supplied argument./demo
page therefore stops computing a redirectTo from redirect_url, and DemoEntry loses that
prop. resolveEntryRedirect itself stays — the gate challenge (/demo/access) still uses it, and
its tests stay with it.admin-only entry in lib/route-policies.ts. It is deliberately not added to
isDemoEntryPath: that list is what makes a path public on the demo host, and this one requires a
session by design./admin. enterViewAs's own admin-only and own-tenant guards still run underneath, unchanged.Everything under the landing is untouched: the session is still the demo admin, the View As banner
and switcher render exactly as they do today, and exiting View As returns the presenter to /admin.
Exit sticks — the auto-entry happens once, on the entry flow's landing, and never on a later
navigation, so a presenter who exits is not pushed back into the vendor view.
/vendor, in View As, without
clicking anything and without an admin screen rendering in between./admin.lib/view-as-switcher-data.test.ts./admin;
nothing errors and no View As cookie is written.enter row through viewAsAuditService carrying both identities
and action: "demo_auto_enter"; a presenter's manual pick still writes its row with no
action.enterViewAs behaves exactly as before for a manual pick — same input shape, same
admin-only and own-tenant refusals, same redirect — with the shared body now in
lib/view-as-enter.ts.admin-only rule in lib/route-policies.ts,
covered by route-policies.test.ts)./admin with no
View As cookie set.not_demo_host, not_gated, disabled, not_configured,
rate_limited — behave exactly as before./admin and is not auto-entered again by navigating
around the demo; only a fresh demo entry auto-enters.resolveEntryRedirect and its tests are unchanged and still serve /demo/access; the /demo
page and DemoEntry no longer carry a redirectTo.DEMO_AUTO_SIGN_IN kill switch.redirect_url ride-along, or the /demo/access gate challenge's use of it.apps/demo — frozen, and not involved.Context budget: over — resolving the entry seam and the View As entry path meant reading
app/(auth)/demo/page.tsx, _components/demo-entry.tsx, app/api/demo/enter/route.ts,
lib/{demo-sign-in,demo-host,view-as,view-as-switcher-data,auth}.ts, lib/actions/view-as.ts and
proxy.ts in full rather than by targeted grep. It was load-bearing: the cookie-setting seam
(route handler vs page) and the single-entry-path refactor are not visible from greps.
03_build/output/notes.mdfeat: demo-auto-vendor-view — land the demo in the vendor viewQuality Project, Project run labels, Review diff against CONVENTIONS.md,
Vercel – web; the other five Vercel projects were skipped — this diff does not touch them)apps/web/lib/view-as-enter.ts (new): applyViewAs — the admin-only and own-tenant guards, the
target lookup, the View As cookie and the dual-identity audit entry, lifted out of the server
action so the demo landing route reaches emulation by the same path a presenter does. Takes an
already-typed target and an optional auditAction label; returns the emulated role rather than
redirecting, because a redirect means different things in an action and a route handler.apps/web/lib/actions/view-as.ts: enterViewAs keeps the Zod validation of client input (the
client-callable surface is unchanged) and the redirect, and delegates the rest to applyViewAs.
exitViewAs is untouched.apps/web/app/api/demo/land/route.ts (new): the demo's landing route. Refuses a non-demo request
by redirecting to the caller's role home, resolves the first vendor, enters View As with
action: "demo_auto_enter", and redirects to /vendor. Falls back to /admin with no cookie
written when the tenant has no vendor or the enter is refused.apps/web/lib/demo-host.ts: DEMO_LANDING_API_ROUTE. Deliberately not added to
isDemoEntryPath — that list makes a path public on the demo host, and this one requires the
session the entry route just minted.apps/web/lib/view-as-switcher-data.ts: firstUserForRole — pure selector over the switcher's
already name-sorted groups, so "first vendor" is deterministic and testable without a harness.apps/web/lib/route-policies.ts: /api/demo/land exact, admin only. Load-bearing, not
ceremony: the route is not public, so without a rule the deny-by-default proxy would bounce the
visitor to /admin — the screen the feature exists to skip.apps/web/app/(auth)/demo/_components/demo-entry.tsx: navigates to the landing route instead of
a redirectTo prop, which is gone.apps/web/app/(auth)/demo/page.tsx: stops reading redirect_url; Props, searchParams and
the local firstValue helper went with it. resolveEntryRedirect itself stays — /demo/access
still uses it.firstUserForRole in lib/view-as-switcher-data.test.ts (name-first pick, order
independence, per-role lookup, empty role, null data) and the landing policy in
lib/route-policies.test.ts (admin-only, nothing granted beyond the exact path)./vendor in View As with no click and no admin screen — DemoEntry navigates to
/api/demo/land, which sets the cookie server-side and redirects on; the browser renders
once, at /vendor.exitViewAs still returns to /admin.firstUserForRole, unit-tested./admin, nothing errors, no cookie — the handler redirects before applyViewAs
is called, so no cookie is written.action: "demo_auto_enter"; a manual pick passes no
action and so records none.enterViewAs unchanged for a manual pick — same input shape, same Zod validation, same
refusals, same redirect.app/api/demo/enter/route.ts is untouched./demo sends them to /admin as it always has.resolveEntryRedirect and its tests unchanged and still serving /demo/access; /demo and
DemoEntry no longer carry a redirectTo.applyViewAs writes it via
cookies(). What did not move is the redirect: the action still redirects, and the route
handler redirects itself, because enterViewAs must keep returning an ActionError to the
switcher while the handler needs its own fallback target. Worth a look that both callers land
where the criteria say.applyViewAs sets the View As
cookie through next/headers and the handler then calls redirect(). That is the same pairing
the server action has always used and the pattern app/api/demo/access/route.ts uses for the
gate cookie, but it is the single point where this feature silently degrades to "lands on
/admin" if Next drops the mutation. Worth confirming on the preview rather than reasoning about.apps/web. The web-route skill records only two
(AI streaming, cron). A demo-host landing helper is neither; it is deliberate and scoped to the
demo branch, not a new general pattern./demo told visitors they would "step in as the
administrator", which this change makes untrue — they now land on a vendor. Reworded to "step
into a vendor's view". Sentence case, no other copy touched.DEMO_HOSTS/?demo=1, the gate cookie, and a demo tenant with at least
one vendor to exercise anything past the entry page.