You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
**Decision**: auth() + redirect() in the Server Component at app/page.tsx.
166
167
**Reason**: Keeps proxy.ts free of per-page logic. Server Component redirect is instant — no client-side flash.
@@ -183,3 +184,36 @@ The following were evaluated and explicitly excluded from MVP. Do not reopen wit
183
184
**Decision**: Downgraded jsdom from v29 (installed by default) to v24 in devDependencies.
184
185
**Reason**: jsdom v29 requires Node >=20.19.0; the project runs on Node 20.11.1. v24 is compatible and supports all required testing features.
185
186
**Alternatives considered**: Upgrade Node (blocked — system constraint); skip component tests in jsdom (violates TDD mandate).
187
+
**SUPERSEDED BY**: "happy-dom for all vitest environments" decision below — jsdom was later abandoned entirely in favour of happy-dom due to a transitive ESM conflict.
188
+
189
+
---
190
+
191
+
## 2026-03-19 — happy-dom for all vitest environments
192
+
**Decision**: All vitest `environmentMatchGlobs` entries use `"happy-dom"` instead of `"jsdom"`.
193
+
**Reason**: `jsdom@29` (installed as transitive dep despite the v24 pin) pulls in `html-encoding-sniffer@6` which does a synchronous `require()` of `@exodus/bytes` (pure-ESM only). This crashes the jsdom environment with an unhandled error that exits vitest with code 1 even when all tests pass — failing CI. `happy-dom` (already installed) has no such transitive dependency and is fully compatible with all existing tests.
194
+
**Effect**: Both `components/__tests__/**/*.test.tsx` and `app/**/*.test.tsx` use `happy-dom`. 100/100 tests pass, zero unhandled errors.
195
+
196
+
---
197
+
198
+
## 2026-03-19 — Proxy loop guard for stale JWT
199
+
**Decision**: In `proxy.ts`, before redirecting to `/signup/complete` when `role` is missing, check `req.nextUrl.pathname === "/signup/complete"` and return early if true.
200
+
**Reason**: When the Clerk session token claim (`{{ user.public_metadata }}`) is not configured in the Clerk dashboard, or when a JWT was issued before a role was assigned, `sessionClaims.metadata.role` is undefined. Without the guard, the proxy redirects to `/signup/complete` even when the user is already there — causing a visible redirect loop. The `/signup/complete` page handles staleness client-side via `session.reload()`.
201
+
**Constraint**: Never call external APIs from `proxy.ts` (it is Next.js middleware running on the edge). The guard is a pure pathname check with no I/O.
**Decision**: Authenticated user redirect on `/` handled via `auth()` + `redirect()` inside the Server Component (`app/page.tsx`), not in `proxy.ts`.
207
+
**Reason**: Keeps proxy.ts free of per-page redirect logic. Server Component redirect is synchronous and instant — no client-side flash. Role read from `(sessionClaims?.metadata as { role?: string })?.role`.
208
+
209
+
---
210
+
211
+
## 2026-03-19 — /signup hash detection for Clerk SSO compatibility
212
+
**Decision**: `/signup/page.tsx` reads `window.location.hash` on mount to decide between showing the role picker (direct visit) or `<SignUp>` (Clerk SSO multi-step flow).
213
+
**Reason**: Clerk SSO flows (Google, phone verification) redirect back to `/signup` with a hash fragment (`#/continue`, `#/factor-one`). Detecting the hash on one route preserves SSO compatibility without adding a new route. Direct visits (no hash) show the pre-auth role picker.
214
+
215
+
---
216
+
217
+
## 2026-03-19 — Button asChild not available; use Link + buttonVariants()
218
+
**Decision**: For anchor-styled buttons (links that look like buttons), use `Link` from `next/link` with `buttonVariants()` class applied directly — not `<Button asChild>`.
219
+
**Reason**: The project's `components/ui/button.tsx` wraps `@base-ui/react/button`, which does not expose a Radix-style `asChild` prop. Applying `buttonVariants()` to a `Link` achieves the same visual result without the prop.
Copy file name to clipboardExpand all lines: .claude/memory/iterations.md
+44-3Lines changed: 44 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -217,9 +217,40 @@ _Append only. One entry per session or PR. Never delete._
217
217
218
218
---
219
219
220
-
## 2026-03-19 — fix/pre-m3-proxy-loop
221
-
- Added /signup/complete loop guard to proxy.ts to prevent redirect loop on stale JWT
222
-
- When authenticated user has no role claim in JWT and is already on /signup/complete, proxy now returns immediately instead of redirecting again
220
+
## 2026-03-19 — fix/pre-m3-proxy-loop (PR #8)
221
+
**Type**: Bug fix
222
+
**Branch**: fix/pre-m3-proxy-loop
223
+
**What changed**:
224
+
-`proxy.ts` — added one-line loop guard: when an authenticated user has no role claim in their JWT and is already on `/signup/complete`, proxy returns immediately instead of redirecting again
225
+
- Prevents the infinite redirect loop that occurs when the Clerk JWT is stale (role claim not yet in token)
226
+
- JWT staleness is handled client-side by `session.reload()` in `/signup/complete` — proxy just stays out of the way
-`app/page.tsx` — replaced Next.js scaffold with branded landing page (Server Component); server-side auth redirect via `auth()` + `redirect()`; hero + 3 benefit blocks + 3 role cards; dark surface, Geist Sans, shadcn/ui Card + buttonVariants
238
+
-`app/(public)/signup/page.tsx` — replaced bare `<SignUp>` with `'use client'` role picker + Clerk SSO fallback; detects Clerk SSO flows via `window.location.hash` on mount (hash present = Clerk driving; no hash = role picker)
239
+
-`components/auth/signup-role-picker.tsx` — pre-auth role selection (Agency / Creator / Brand) with shadcn/ui Cards and correct hrefs; Server Component
240
+
-`components/auth/auth-layout.tsx` — shared two-panel auth layout Server Component (branded left panel hidden on mobile via `hidden md:flex`, Clerk right panel)
241
+
-`app/(public)/login/[[...rest]]/page.tsx` — wrapped in AuthLayout; forceRedirectUrl unchanged
242
+
-`app/(public)/signup/agency/[[...rest]]/page.tsx` — wrapped in AuthLayout; forceRedirectUrl unchanged
243
+
-`app/(public)/signup/creator/[[...rest]]/page.tsx` — wrapped in AuthLayout; forceRedirectUrl unchanged
244
+
-`app/(public)/signup/brand/[[...rest]]/page.tsx` — wrapped in AuthLayout; forceRedirectUrl unchanged
245
+
-`app/page.test.tsx` — 9 vitest tests for landing page
246
+
-`components/__tests__/auth-layout.test.tsx` — 3 vitest tests for AuthLayout
247
+
-`components/__tests__/signup-role-picker.test.tsx` — 3 vitest tests for SignupRolePicker
248
+
-`vitest.config.ts` — switched `app/**/*.test.tsx` and `components/__tests__/**/*.test.tsx` to `happy-dom` (from `jsdom`) to resolve CJS/ESM crash from `html-encoding-sniffer` v6 in jsdom v29
-`app/page.test.tsx` line 70: tightened regex from `/get started/i` to `/^get started$/i` — the broad regex matched all 4 "Get started" links on the landing page causing `getByRole` to throw "Found multiple elements"
278
+
-`vitest.config.ts`: switched `components/__tests__/**/*.test.tsx` from `jsdom` to `happy-dom` to eliminate 5 unhandled errors from `html-encoding-sniffer` v6's synchronous `require()` of `@exodus/bytes` (pure-ESM); both globs now use `happy-dom`
279
+
- All 100 tests pass, zero unhandled errors, CI green
0 commit comments