# Journey: First-time anonymous visitor

The end-to-end path from an unknown browser hitting the domain through completed
onboarding and a first landing on Home. Catches public-surface gating, OpenGraph
crawler exposure, sign-up funnel telemetry, the magic-link / email verification
round-trip, and the onboarding handoff into the signed-in shell — bugs that look
fine in any single per-view file but stack into broken funnels.

## Personas

- **Marketing-campaign click-through** — primary; lands on `/welcome` or
  `/landing` from a paid ad
- **Editorial reader** — lands on `/landing`, reads the manuscript lede before
  starting sign-up
- **Crawler / link-preview bot** — verifies OpenGraph and Twitter card metadata
  resolve before any human visits
- **Direct-URL signed-in route attempt** — anonymous user pastes `/tara` or
  `/library`; middleware must redirect with `?redirect=` preserved

## Pre-conditions

- Browser has never visited `oshun.app`; no cookies, no service worker, no
  IndexedDB
- Network is online
- A fresh, deliverable inbox is available for the magic-link / verification
  email
- BFF and outbound mail (Mailpit in dev) are healthy
- `PUBLIC_PATHS` and `PUBLIC_PREFIXES` in `proxy.ts` match the deployed build.
  Note: the implementation lives only in `apps/oshun/web/src/proxy.ts` — there
  is no `middleware.ts`. Next 16 picks up the exported `proxy` function via the
  `config` matcher natively.

## Steps

### 1. Crawler / link-preview pre-flight

- [x] Issue a `GET /` with a `User-Agent: facebookexternalhit/1.1` and verify it
      resolves to public HTML at `/` or the anonymous `/welcome` bounce
- [x] Issue a `GET /welcome` and inspect `<head>` for OpenGraph + Twitter card
      tags pointing at `/welcome/opengraph-image` and `/welcome/twitter-image`
- [x] Issue `GET /opengraph-image`, `GET /twitter-image`,
      `GET /welcome/opengraph-image`, and `GET /welcome/twitter-image`; confirm
      200 + PNG content-type
- [x] Confirm `metadataBase` is `https://oshun.app` so canonical URLs in
      `<link rel="canonical">` resolve absolute; `/welcome` emits
      `https://oshun.app/welcome`
- [x] Confirm JSON-LD `WebApplication` includes a free `Offer` and a
      `featureList` mentioning all six domains (tara, arete, veritas, nyx,
      nisaba, metis)
- [x] **Verify**: [`shell/01-app-shell.md`](../shell/01-app-shell.md) Metadata
      and SEO section
- [x] **Verify**:
      [`customer/00-public/welcome.md`](../customer/00-public/welcome.md)
      OpenGraph route handlers

### 2. Cold visit — anonymous user lands on `/`

- [x] Browser navigates to the root URL
- [x] Middleware sees no session cookie and redirects to `/welcome?redirect=%2F`
- [x] Cookie consent banner mounts at bottom of viewport until accept/reject
- [x] Service-worker support is present and `/sw.js` resolves with a JavaScript
      content-type; manifest and install assets are verified by the PWA smoke
      suite
- [x] Theme color is emitted in metadata; root layout defines cream `#f1ebdd`
      (light) and `#241c12` (dark)
- [x] Cormorant Garamond, Inter, and JetBrains Mono are loaded through
      `next/font` with `display: 'swap'`
- [x] `<html lang="en" dir="ltr">` is emitted by the root layout
- [x] **Verify**:
      [`shell/02-routing-layouts.md`](../shell/02-routing-layouts.md) Middleware
      → Public path allowlist

### 3. Anonymous attempt at a gated route preserves the deep link

Before signing up, the user pastes a deep link to a signed-in route to test
gating.

- [x] Browser navigates to a protected route such as `/profile`
- [x] Middleware redirects to `/welcome?redirect=%2Fprofile`
- [x] `WelcomePageView` reads the `redirect` query via the page server
      component; `WelcomeAuthPanel` carries the redirect through
      `buildPublicAuthEntryPath`
- [x] Domain launch CTAs preserve encoded deep links such as
      `/domains/nisaba?origin=home`
- [x] `?expired=1` and `?reauth=1` variants force sign-in mode; the redirect
      param is sanitized by `sanitizeRedirectPath`
- [x] **Verify**: [`shell/04-auth-session.md`](../shell/04-auth-session.md)
      Session expired / Sign-in flow / Sign-in success

### 4. Editorial / marketing route browse (optional branch)

- [x] Navigate to `/landing` directly
- [x] `MarketingLanding` renders the OSHUN masthead, product preview strip,
      three-column manuscript lede, six room inventory, three static launch
      scenarios, and three-tier membership row
- [x] Pricing tier CTAs link to `/welcome?mode=signup` with
      `entry=marketing-landing` and exact `tier=Solo|Hearth|Institutional` query
      params
- [x] The Hearth tier CTA records `public_auth_funnel_cta_clicked` with
      `step=marketing_tier_signup`
- [x] The text-only marketing footer renders without descendant legal anchors
- [x] A warmed `/landing` document replays from the real service worker while
      offline; standalone 390 px launch has no horizontal overflow and keeps
      conversion targets at least 44 px
- [x] Cookie consent banner persists across `/landing` → `/welcome` navigation
- [x] **Verify**:
      [`customer/00-public/landing.md`](../customer/00-public/landing.md)
      Pricing tier CTAs

### 5. Accept (or reject) cookie consent

- [x] Click "Accept" on the `CookieConsentBanner`
- [x] The banner persists a versioned localStorage preference with essential,
      analytics, functional, and marketing all allowed, then dismisses. It does
      not itself create analytics cookies; downstream code must consult the
      stored permission before optional processing.
- [x] Reload — banner does not reappear
- [x] Reject branch persists essential-only permission with analytics,
      functional, and marketing false; the banner dismisses.
- [x] `PwaUpdatePrompt` is suppressed only while consent is unresolved; accept
      or reject both resolve consent, so prompts become eligible after either
      decision
- [x] **Verify**: [`shell/04-auth-session.md`](../shell/04-auth-session.md)
      Cookie consent gate

### 6. Choose sign-up from `/welcome`

- [x] Default mode resolves to `signup` (no `?mode=` query)
- [x] `WelcomeAuthPanel` shows the Sign up / Sign in / Recover tab list
- [x] Switching tabs reflects in URL state; Left/Right arrows navigate the
      tablist
- [x] Enter name / email / password
- [x] Public CTAs fire `trackPublicAuthFunnelCtaClicked({ entrySource, mode })`;
      form submission and completion fire `public_auth_funnel_submitted` and
      `public_auth_funnel_completed`
- [x] `WelcomeAuthPanel` calls `useAuth().signUp`, which posts to
      `/api/auth/signup`; the Next route forwards to BFF `/v1/auth/signup`,
      creates the session cookies, sends the verification email, and routes the
      signed-in browser to `/onboarding`
- [x] **Verify**:
      [`customer/00-public/welcome.md`](../customer/00-public/welcome.md)
      WelcomeAuthPanel mode tabs + submit

### 7. Email verification round-trip

- [x] Mailpit receives the verification email with subject
      `Verify your Oshun email address`; the delivered link preserves
      `?next=/tara` (or the original redirect)
- [x] Click the actual verification link from the delivered email
- [x] BFF marks the account verified; the signup browser already has HttpOnly
      `oshun-session` and `oshun-access` cookies, and a fresh browser opening
      the link receives fresh HttpOnly `oshun-session` and `oshun-access`
      cookies for that device
- [x] The verify-email page refreshes `/api/auth/session` and reads
      `verified: true`
- [x] User is bounced to `/onboarding?redirect=/tara` (not the original `?next=`
      surface yet — the shell intercepts unfinished onboarding first)
- [x] **Verify**: [`shell/04-auth-session.md`](../shell/04-auth-session.md)
      Sign-up flow

### 8. Onboarding wizard — 10 steps

`OnboardingWizard` is the only thing `/onboarding/page.tsx` renders.

- [x] **Welcome** step — "Begin" CTA primary
- [x] **Goals** — multi-select goal cards (`OnboardingGoalId`)
- [x] **Domains** — six domain tiles (`OshunDomainId`) match
      `getShellNavigationDomains()`
- [x] **Interests** — multi-select interest chips, each with a
      recommended-domains hint
- [x] **Rhythm (Routine)** — daypart timing picker (`OnboardingRoutineTimingId`)
- [x] **Guide** — assistant persona, teacher tone, content tone preferences
- [x] **Alerts (Notifications)** — `OnboardingNotificationPreferences` channels
- [x] **Access (Accessibility)** — `OnboardingAccessibilityNeeds` toggles
- [x] **Memory** — `OnboardingMemoryPreferences` scope choices
- [x] **Ready (Complete)** — final "Finish" submit calls
      `patchProfilePreferences(patch)` and writes the local
      `oshunWebPreferencesStore`
- [x] Back / Continue / Finish keyboard focus order is sane; Back disabled on
      first step
- [x] Step deep link `?step=memory` mounts directly at that step
- [x] **Verify**:
      [`customer/01-onboarding/onboarding.md`](../customer/01-onboarding/onboarding.md)
      step-by-step interactions

### 9. Onboarding completion handoff

- [x] After Finish, the wizard navigates to the sanitised `redirectPath` if set
      (e.g. `/tara` from step 3) — otherwise to
      `resolveFirstRunEntryTarget(entrySource)`
- [x] No `?next=` leak in the URL after handoff
- [x] **Verify**:
      [`customer/01-onboarding/onboarding.md`](../customer/01-onboarding/onboarding.md)
      Completion handoff

### 10. First landing on signed-in surface

- [x] If redirect was `/profile`, `ProfileDashboard` renders after onboarding
      completion
- [x] If redirect was a public domain launch, the requested domain surface
      renders; the covered Nisaba branch lands at `/domains/nisaba?origin=home`
- [x] If redirect was absent or `/`, `resolveFirstRunEntryTarget(entrySource)`
      chooses the deliberate first-run domain instead of dropping context
- [x] If redirect was `/tara`, the Tara journey coverage takes over and verifies
      today's sit, course, and teachers
- [x] If Home is the final target, `HomeWorkspace` renders Tara ritual
      continuation, Arete practice, Nyx perspective, Metis study continuation,
      Daypart rail, Domain card grid, Quick actions, Activity feed
- [x] `OnboardingResumeBanner` is hidden (onboarding now complete)
- [x] Shell header shows the five primary tabs with `aria-current="page"` on the
      active one
- [x] Mobile bottom nav appears on coarse pointer + viewport <= 640 px
- [x] **Verify**:
      [`customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)
      Returning user, full data
- [x] **Verify**: [`customer/03-tara/tara.md`](../customer/03-tara/tara.md)
      Today's sit card

### 11. Telemetry funnel trail

- [x] `WelcomePageView`, `PublicAuthFunnelLink`, `MarketingAuthCta`, and
      `WelcomeAuthPanel` emit the verified public-auth events: viewed, CTA
      clicked, submitted, completed, and failed.
- [x] The consent decision is durably represented by the versioned
      `oshun-cookie-consent` preference record.
- [ ] Emit and verify a dedicated consent analytics event. No
      `cookie_consent_accepted` / `cookie_consent_rejected` symbol exists in the
      current source.
- [ ] Wire the exported onboarding step/completion telemetry helpers into
      `OnboardingWizard`; exporting them from `taraAnalytics.ts` is not proof of
      emission.
- [ ] Emit a service-worker registration event. The current PWA telemetry covers
      sync queued and update applied, not `pwa_sw_registered`.
- [ ] Define and emit the first signed-in landing event. Neither
      `home_dashboard_loaded` nor `tara_hub_viewed` exists in the current web
      source.

## Post-conditions

- User has a valid session cookie scoped to the host
- Onboarding is complete; `oshunWebPreferencesStore` reflects the user's choices
- The auth proxy admits signed-in routes and preserves safe redirect targets.
  Entitlement checks remain owned by downstream route/surface gates; the proxy
  does not validate every product tier globally.
- Cookie consent decision persisted
- Service worker registered with `oshun-static-v<N>` precaching the public shell
- No leaked `?redirect=` or `?next=` query params in the address bar after
  handoff

## Failure modes to verify

- [x] **Magic-link email never arrives** — `/welcome` exposes recover-mode
      affordances so the user can request access recovery; recovery request and
      confirm hit the real BFF routes
- [x] **User clicks verification link on a different device than they signed up
      from** — verification succeeds; new device gets a fresh session; original
      device can return manually and refresh session state
- [x] **Onboarding abandoned mid-wizard** — return to `/` later; the home
      `OnboardingResumeBanner` "Continue onboarding" CTA links into
      `/onboarding?mode=resume` at the last incomplete step
- [x] **Crawler hits `/welcome` with no `Accept-Encoding` for images** —
      `/welcome/opengraph-image` route handler still responds with a usable
      static image
- [x] **Cookie-consent unresolved** — analytics CTAs still fire local events but
      no remote tracking pixels load; `PwaInstallPrompt` and `PwaUpdatePrompt`
      stay suppressed only while consent is undecided (`PwaBootstrap.tsx:242`
      gates on `cookieConsent !== null`). Deciding — accept OR reject — makes
      the prompts eligible; reject resolves consent rather than suppressing
      them.
- [x] **Signed-in user pastes `/welcome` URL** — current behavior is intentional
      public-route rendering: middleware does not bounce signed-in users away
      from `/welcome`; the page renders `[data-auth-entry-signed-in]` with the
      continuation action.

## E2E coverage

- Backed by
  [`apps/oshun/web/e2e/auth-entry-flows.spec.ts`](../../apps/oshun/web/e2e/auth-entry-flows.spec.ts),
  [`apps/oshun/web/e2e/welcome-marketing.spec.ts`](../../apps/oshun/web/e2e/welcome-marketing.spec.ts),
  [`apps/oshun/web/e2e/pwa-smoke.spec.ts`](../../apps/oshun/web/e2e/pwa-smoke.spec.ts),
  and
  [`apps/oshun/web/e2e/cookie-consent-compliance.spec.ts`](../../apps/oshun/web/e2e/cookie-consent-compliance.spec.ts)
  — together these exercise the redirect-to-welcome gate, sign-up and onboarding
  completion, OpenGraph/Twitter metadata, install prompt basics, and
  cookie-consent accept/reject storage.
- Backed by
  [`apps/oshun/web/e2e/first-time-visitor-deepening.spec.ts`](../../apps/oshun/web/e2e/first-time-visitor-deepening.spec.ts)
  — drives the step-1 crawler-facing JSON-LD `featureList` (asserts it lists all
  six domains under a `facebookexternalhit` UA), the steps 3–4 expired-session
  re-entry (`/welcome?redirect=%2Ftara&expired=1` opens with the signin tab
  selected plus the session-expired banner and re-entry guide), the `/landing`
  primary CTA carrying `entry=marketing-landing` through to `/welcome`, and the
  failure mode where a signed-in user visiting `/welcome` stays on `/welcome`
  and sees the continuation panel rather than being redirected.
- Backed by
  [`apps/oshun/web/e2e/public-marketing.spec.ts`](../../apps/oshun/web/e2e/public-marketing.spec.ts)
  — drives the dedicated `/landing` branch: fixture-backed six-room inventory,
  static letter scenarios, all three tier CTA href contracts, Hearth tier-click
  telemetry, text-only footer boundary, 390 px standalone no-overflow and 44 px
  CTA targets, real `sw.js` warm-cache offline replay, social metadata/PNG
  previews, and sitemap exposure.
- Backed by
  [`apps/oshun/web/e2e/first-time-deepening-2.spec.ts`](../../apps/oshun/web/e2e/first-time-deepening-2.spec.ts)
  — drives the `?reauth=1` sign-in re-entry variant plus reauth-specific banner
  and asserts the crawler JSON-LD shape is a schema.org `WebApplication` with a
  free `Offer`.
- Backed by
  [`apps/oshun/web/e2e/email-verify-mailpit.spec.ts`](../../apps/oshun/web/e2e/email-verify-mailpit.spec.ts)
  — drives the real dev-infra email leg for step 7: `/welcome` signup creates
  the browser's HttpOnly `oshun-session` and `oshun-access` cookies, the BFF
  sends the verification email over SMTP to Mailpit (`:1025`), the spec polls
  Mailpit's API (`:8025`) by the unique recipient address, opens the actual
  delivered `/auth/verify-email?token=...&next=/tara` link, confirms the public
  BFF token through the verify-email page, auto-hands the already signed-in
  browser back to `/onboarding?redirect=/tara` without clicking Continue,
  refreshes `/api/auth/session`, and asserts the account is now
  `verified: true`. It also opens the same delivered Mailpit link in a fresh
  browser context with no pre-existing auth cookies and proves the verify-email
  proxy sets fresh HttpOnly `oshun-session` and `oshun-access` cookies for that
  device before landing in `/onboarding?redirect=/tara`. The signup handoff to
  `/onboarding` is also asserted before the email link is opened.
- Backed by
  [`apps/oshun/web/e2e/email-verify-roundtrip.spec.ts`](../../apps/oshun/web/e2e/email-verify-roundtrip.spec.ts)
  — drives the verify-email page-level states and failure modes, including a
  fresh token success, replayed-token error, no-token missing state, delayed
  verifying state, already-verified copy, safe `next=/tara` Continue handoff,
  unsafe external `next` fallback to `/`, and a 44x44 px Continue target at a
  390 px viewport.
- Backed by
  [`apps/oshun/web/e2e/onboarding-lifecycle.spec.ts`](../../apps/oshun/web/e2e/onboarding-lifecycle.spec.ts)
  — drives the abandonment failure mode: real signup against the dev BFF,
  leaving a draft at Rhythm, Home `OnboardingResumeBanner` return to
  `/onboarding?mode=resume`, and returning sign-in resuming with
  `redirect=/profile` preserved.
- Backed by
  [`apps/oshun/web/e2e/public-legal-pages.spec.ts`](../../apps/oshun/web/e2e/public-legal-pages.spec.ts)
  — drives the public legal branch reached from footer/cookie-consent links:
  anonymous access for `/legal/privacy`, `/legal/terms`, `/legal/cookies`,
  `/legal/accessibility`, `/legal/ccpa`, `/legal/dpa`, `/legal/lilith`, and
  `/legal/lilith/privacy`; exact section inventories; cross-legal nav;
  TOC/back-to-top/print shell controls; 390 px no-overflow and 44 px targets;
  offline-after-load stability; no telemetry hooks; axe; and the Lilith
  non-clickable footer markers.
- **Coverage depth**: deep — see [`coverage.md`](./coverage.md).
- **Remaining delegation**: shell chrome, full Home dashboard detail, and the
  Tara daily-sit content are verified in their own shell/home/Tara walkthroughs
  and specs; this journey verifies the first-time handoff into those surfaces.

## Per-view files touched by this journey

- [`customer/00-public/landing.md`](../customer/00-public/landing.md) —
  editorial entry / pricing CTAs
- [`customer/00-public/welcome.md`](../customer/00-public/welcome.md) —
  auth-coupled marketing + `WelcomeAuthPanel`
- [`customer/00-public/welcome-download.md`](../customer/00-public/welcome-download.md)
  — install / PWA install branch for users who choose download instead of
  sign-up
- [`customer/00-public/legal-privacy.md`](../customer/00-public/legal-privacy.md)
  — policy linked from cookie consent; covered by `public-legal-pages`
- [`customer/00-public/legal-cookies.md`](../customer/00-public/legal-cookies.md)
  — cookie consent detail; covered by `public-legal-pages`
- [`customer/01-onboarding/onboarding.md`](../customer/01-onboarding/onboarding.md)
  — the 10-step wizard
- [`customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)
  — first signed-in landing
- [`customer/03-tara/tara.md`](../customer/03-tara/tara.md) — Tara as the most
  common signup redirect target

## Cross-references

- Feature spec:
  [`V1/features.md`](../../V1/features.md#public-web-and-distribution)
- Architecture:
  [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#customer-web--appsoshunweb)
- Related journeys:
  - [`install-as-pwa.md`](./install-as-pwa.md) — the install branch follows once
    the user is signed in
  - [`first-tara-sit.md`](./first-tara-sit.md) — the most common next journey
- Shell docs:
  - [`shell/01-app-shell.md`](../shell/01-app-shell.md)
  - [`shell/02-routing-layouts.md`](../shell/02-routing-layouts.md)
  - [`shell/04-auth-session.md`](../shell/04-auth-session.md)
- Component sources:
  - `apps/oshun/web/src/proxy.ts`
  - `apps/oshun/web/src/app/welcome/page.tsx`
  - `apps/oshun/web/src/components/welcome/WelcomePageView.tsx`
  - `apps/oshun/web/src/components/welcome/WelcomeAuthPanel.tsx`
  - `apps/oshun/web/src/components/onboarding/OnboardingWizard.tsx`
  - `apps/oshun/web/src/lib/onboarding-routing.ts`
  - `apps/oshun/web/src/profile/preferences-sync.ts`
  - `apps/oshun/web/src/components/CookieConsentBanner.tsx`

## Open questions / known gaps

- [x] **Anonymous gate lives in `proxy.ts`** — `proxy.ts` holds the full
      anonymous-gate logic (PUBLIC_PATHS, redirect-to-welcome, expiry, studio
      boundary) and exports `proxy` + a `config` matcher, which Next 16 picks up
      natively (there is no `middleware.ts`). Covered by
      `src/__tests__/middleware-public-paths.test.ts`.
- [x] **OpenGraph / Twitter image routes were crashing** in `@vercel/og` due to
      `var(--l-*)` CSS variables inside `social-preview.tsx`. Satori has no CSS
      context, so it choked with
      `TypeError: Cannot read properties of null     (reading '1')`. Replaced
      with literal hex/rgba constants (kept in sync with the cream variant in
      `design-system/lilith/lilith.css`).
- [x] **WelcomeAuthPanel auth endpoints** — the panel calls `useAuth()`;
      `signUp` posts `/api/auth/signup` -> BFF `/v1/auth/signup`, `signIn` posts
      `/api/auth/login` -> BFF `/v1/auth/login`, `requestRecovery` posts
      `/api/auth/recovery/request` -> BFF `/v1/auth/recovery/request`, and
      `recoverAccount` posts `/api/auth/recovery/confirm` -> BFF
      `/v1/auth/recovery/confirm`.
- [x] **Signed-in `/welcome` behavior** — `proxy.ts` keeps `/welcome` public;
      signed-in users stay on `/welcome` and see `[data-auth-entry-signed-in]`,
      covered by `first-time-visitor-deepening`.
- [x] **Onboarding save semantics** — per-step drafts persist to
      `oshun.onboarding`; final submit writes the local preference store and
      syncs via `/v1/preferences`, covered by `onboarding-lifecycle` and
      `onboarding-deepening`.
- [x] **Cookie rejection and PWA prompts** — reject resolves consent; update
      prompts are suppressed only while consent is unresolved, covered by
      `pwa-install-update-offline`.
- [x] **Mailpit verification subject/copy** — Mailpit specs assert subject
      `Verify your Oshun email address` for the unique recipient and extract the
      delivered `/auth/verify-email?token=...` link from the message body.
- [ ] Connect the onboarding and first-landing telemetry declared in the feature
      contract to their real UI actions, then add browser assertions for the
      emitted payloads.
