Problem
Password recovery is broken — the Forgot Password link on the sign-in page is a placeholder, not a working flow. A user who forgets their password today has no recovery path; they're locked out until an admin runs an SQL update by hand.
Audit:
| Surface |
State |
Sign-in link at web/src/components/auth/sign-in-form.tsx:89-94 |
<a href="#"> — clicking it does nothing |
/forgot-password route |
Doesn't exist |
/reset-password route |
Doesn't exist |
supabase.auth.resetPasswordForEmail() call |
Nowhere in the codebase |
| Supabase Reset Password email template |
Default content; URL not pointed at our app (we never wired this) |
Scope
Build the standard Supabase password-reset flow end-to-end.
Route 1 — request a reset
web/src/app/[locale]/(marketing)/forgot-password/page.tsx — single email input. On submit:
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${APP_URL}/auth/confirm?next=/reset-password`,
});
Always show a generic "If that email exists, we've sent a reset link" message regardless of whether the email is registered — prevents account enumeration.
Route 2 — set the new password
web/src/app/[locale]/(marketing)/reset-password/page.tsx — password + confirm inputs. On submit:
await supabase.auth.updateUser({ password: newPassword });
The session is already established by /auth/confirm (the same OTP-verify route that powers invite acceptance — see #129), so updateUser works directly.
Supabase email template
In Supabase Dashboard → Authentication → Emails → Reset Password template, switch the link href from {{ .ConfirmationURL }} to:
{{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=recovery
Same shape as the Invite user template we rewired in #129. The /auth/confirm route already handles type=recovery via verifyOtp({ type, token_hash }).
Wire the link
In sign-in-form.tsx:89-94, replace href="#" with <Link href="/forgot-password">.
Acceptance criteria
Notes / not a good first issue
This touches auth flows, the shared /auth/confirm route, Supabase email templates, and account-enumeration concerns (the "generic response on submit" rule above is important — don't leak which emails are registered). Someone picking it up should be comfortable with the verifyOtp pattern in #129.
Out of scope
- Magic-link login (separate auth method, not what's broken here)
- Password strength meter on the new-password input (existing forms don't have one either; add separately)
- Localized email body — we only ship one locale right now
Problem
Password recovery is broken — the Forgot Password link on the sign-in page is a placeholder, not a working flow. A user who forgets their password today has no recovery path; they're locked out until an admin runs an SQL update by hand.
Audit:
web/src/components/auth/sign-in-form.tsx:89-94<a href="#">— clicking it does nothing/forgot-passwordroute/reset-passwordroutesupabase.auth.resetPasswordForEmail()callScope
Build the standard Supabase password-reset flow end-to-end.
Route 1 — request a reset
web/src/app/[locale]/(marketing)/forgot-password/page.tsx— single email input. On submit:Always show a generic "If that email exists, we've sent a reset link" message regardless of whether the email is registered — prevents account enumeration.
Route 2 — set the new password
web/src/app/[locale]/(marketing)/reset-password/page.tsx— password + confirm inputs. On submit:The session is already established by
/auth/confirm(the same OTP-verify route that powers invite acceptance — see #129), soupdateUserworks directly.Supabase email template
In Supabase Dashboard → Authentication → Emails → Reset Password template, switch the link href from
{{ .ConfirmationURL }}to:Same shape as the Invite user template we rewired in #129. The
/auth/confirmroute already handlestype=recoveryviaverifyOtp({ type, token_hash }).Wire the link
In
sign-in-form.tsx:89-94, replacehref="#"with<Link href="/forgot-password">.Acceptance criteria
/sign-innavigates to/forgot-passwordresetPasswordForEmailand shows the generic "we sent a link if the account exists" message regardless of email validity/reset-passwordwith an active session (no/sign-updetour)pnpm tsc --noEmit+yarn lintclean/invite/<token>(the/auth/confirmroute is shared)Notes / not a good first issue
This touches auth flows, the shared
/auth/confirmroute, Supabase email templates, and account-enumeration concerns (the "generic response on submit" rule above is important — don't leak which emails are registered). Someone picking it up should be comfortable with the verifyOtp pattern in #129.Out of scope