Skip to Content

← All archived runs

Run: demo-auto-vendor-view

run.md

Run: demo-auto-vendor-view

  • branch: claude/demo-auto-vendor-view-wtuxkm
  • pr: #878

02_define/output/spec.md

Spec: Demo lands in the vendor view

  • slug: demo-auto-vendor-view
  • personas: Vendor, Admin
  • touches: apps/web/app/(auth)/demo, apps/web/app/api/demo, apps/web/lib, apps/web/lib/actions
  • complexity: standard

Problem

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

Proposed change

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:

  • Where it runs. A new demo-only landing route handler, /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.
  • Which vendor. The first vendor in the View As switcher's existing ordering — 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.
  • One entry path. The landing route does not write the View As cookie itself. The body of 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.
  • Audited as automatic. The auto-entry records an ordinary 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.
  • Deep links. On the demo host the vendor landing wins unconditionally: a completed demo entry always goes through the landing route, whatever path the visitor originally asked for. The /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.
  • Route policy. The landing route is behind authentication like any other app route, so it gets an 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.
  • Guards and fallbacks. The handler refuses anything that is not a demo request by redirecting the caller to their own role home — it is a post-authentication landing helper, not a token mint, so there is nothing to conceal and the caller always ends up somewhere sensible. When the demo tenant's directory holds no vendor, no cookie is written and the visitor is redirected to /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.

Acceptance criteria

  • A visitor who completes demo entry on the demo host lands on /vendor, in View As, without clicking anything and without an admin screen rendering in between.
  • The underlying session is still the demo admin: the View As banner shows the emulated vendor, the switcher offers every other emulatable persona, and exiting View As returns the presenter to /admin.
  • The vendor chosen is the first of the switcher's name-sorted vendor group, so the same directory always yields the same vendor; a pure selector for it is unit-tested in lib/view-as-switcher-data.test.ts.
  • With no vendor in the demo tenant's directory, entry still succeeds and lands on /admin; nothing errors and no View As cookie is written.
  • The auto-entry writes one 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.
  • The landing route redirects a caller to their own role home when the request is not a demo request, and is reachable only by an admin (an admin-only rule in lib/route-policies.ts, covered by route-policies.test.ts).
  • Nothing changes on the platform host: a real admin signs in and lands on /admin with no View As cookie set.
  • The existing entry refusals — not_demo_host, not_gated, disabled, not_configured, rate_limited — behave exactly as before.
  • A presenter who exits View As stays on /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.

Out of scope

  • Changing which user the demo signs in as, or any part of the entry gate, token mint, rate limit or the DEMO_AUTO_SIGN_IN kill switch.
  • View As on the platform host, or for real admins on real tenants.
  • Making the auto-entered persona configurable, pinning a specific vendor seat by environment variable or roster, or auto-entering any persona other than vendor.
  • Any change to the vendor dashboard itself or to what demo data it shows.
  • Changing the proxy's redirect_url ride-along, or the /demo/access gate challenge's use of it.
  • apps/demo — frozen, and not involved.

Open questions

  • none

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

Build notes: demo-auto-vendor-view

  • commits: feat: demo-auto-vendor-view — land the demo in the vendor view
  • ci: GREEN on 37f0737 (Quality Project, Project run labels, Review diff against CONVENTIONS.md, Vercel – web; the other five Vercel projects were skipped — this diff does not touch them)

What changed

  • 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.
  • Tests: 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).

Acceptance criteria status

  • Lands on /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.
  • Session is still the demo admin — nothing about the sign-in changed; the banner and switcher read the same cookie they always have, and exitViewAs still returns to /admin.
  • Vendor is the first of the name-sorted vendor group — firstUserForRole, unit-tested.
  • No vendor → /admin, nothing errors, no cookie — the handler redirects before applyViewAs is called, so no cookie is written.
  • Audit row carries both identities and 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.
  • Landing route redirects a non-demo caller to their role home and is admin-only — policy rule plus tests.
  • Nothing changes on the platform host — the landing route is never navigated to there, and refuses by redirecting if it is.
  • Entry refusals unchanged — app/api/demo/enter/route.ts is untouched.
  • Exit sticks — auto-entry happens only on the entry flow's landing. A presenter who exits has no cookie, so /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.

Notes for Verify

  • One deviation from the spec's wording, same substance. The spec said the cookie write moves into the shared module along with the guards and the audit. It does — 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.
  • The cookie-then-redirect mechanic is the thing to smoke. 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.
  • This is a third kind of API route in 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.
  • One copy change beyond the criteria. /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.
  • Not smoke-testable from CI: the acceptance criteria all turn on a real Clerk session on the demo host. The preview needs DEMO_HOSTS/?demo=1, the gate cookie, and a demo tenant with at least one vendor to exercise anything past the entry page.