Symptoms
- A user clicks “Forgot password?”, gets the email, clicks the link, and lands on your home page or dashboard. No “choose a new password” form.
- The link in the email ends with
redirect_to=https://yourapp.com(no/update-passwordor/reset-passwordon the end). - It works on localhost or in the Lovable, Bolt or Replit preview, but not on your live domain.
- The user is now signed in, but never set a new password.
Why it happens
The reset goes: your app → Supabase sends the email → user clicks → Supabase checks the link → Supabase sends the user back to your app → your app shows the reset form.
There are four common places it goes wrong:
- No
redirectToin the code. Supabase's docs say the Site URL “defines the default redirect URL when noredirectTois specified”, and that this setting “is critical for email confirmations and password resets”. So with noredirectTo, the link always opens your Site URL, which is your home page. redirectTois not on the Redirect URLs list. TheredirectToaddress “should match the Redirect URLs list”. If it doesn't, people are sent to the Site URL instead. Common mismatches: onlyhttps://yourapp.comis listed but you ask forhttps://yourapp.com/update-password;www.is missing;httpinstead ofhttps; the address has nohttps://at all; or the list still only has the preview or localhost address. A single*does not cover deeper paths:https://yourapp.com/*matches/update-passwordbut not/auth/update-password.- The email template ignores
redirectTo. If you changed the “Reset Password” email template and the link is built from{{ .SiteURL }}with nothing after it, every link goes to the home page. Supabase's docs say that when you useredirectToyou may need to use{{ .RedirectTo }}in the template instead. - Your app sends signed-in users to the home page. When the reset link works, the user arrives already signed in (that is how they're allowed to set a new password). Many AI-built apps have a rule like “if signed in, go to the dashboard” or “on SIGNED_IN, go home”. That rule runs first and the reset page never shows. Supabase sends a
PASSWORD_RECOVERYevent for exactly this case, and its docs say to “show screen to update user's password” when it arrives.
Step-by-step fix
1. Look at the link in the email
Send yourself a reset email from the live site. Don't click the link. Copy it (right-click, “Copy link address”) and paste it somewhere you can read it. Look for redirect_to=.
redirect_to=https://yourapp.comor your home page: go to steps 2 and 3.redirect_to=https://yourapp.com/update-password(the right page) but you still land on home: go to step 5.redirect_to=http://localhost:3000: your Site URL is still localhost. See Why does my Supabase email link send people to localhost:3000?
2. Set the Site URL and add the reset page to Redirect URLs
In Supabase → Authentication → URL Configuration:
- Site URL: your live address, for example
https://yourapp.com. - Redirect URLs: add the exact reset page, for example
https://yourapp.com/update-password. If people also usewww., addhttps://www.yourapp.com/update-passwordtoo. Keep your preview address on the list if you test there.
Supabase recommends the exact path for production. https://yourapp.com/** also works, but it allows every page on your site.
3. Pass redirectTo in the reset request
Find the code that sends the reset email. It should look like this:
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/update-password`,
})Use window.location.origin (or your live address) so it is correct on every domain. Don't hardcode a preview or localhost address. The value must start with https://.
4. Check the email template (only if you changed it)
In Supabase → Authentication → Emails → Reset Password, look at the link. If it is {{ .SiteURL }} on its own, change it to use {{ .RedirectTo }}, or go back to the default template, which already uses the right link. If your app uses the server-side (PKCE) setup, Supabase's template is:
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recovery&next=/update-password">Reset password</a>Here the next= part decides the page, and your /auth/confirm route must send the user to next after verifyOtp succeeds.
5. Make the reset page show before any “go home” rule
On the reset page (or once in your app's sign-in setup), listen for the recovery event and show the form:
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'PASSWORD_RECOVERY') {
// show the "choose a new password" form, don't redirect
}
})Then check every place that redirects signed-in users (route guards, “if session, navigate to /dashboard”, a SIGNED_IN handler). Add the reset page as an exception, or check for PASSWORD_RECOVERY first. When the user submits the new password, call:
await supabase.auth.updateUser({ password: newPassword })and only then send them to the home page or dashboard.
If your app uses the PKCE flow, the user arrives with ?code=... in the address. That code is swapped for a session with exchangeCodeForSession. Supabase says it is valid for 5 minutes, works once, and must be used in the same browser that asked for the reset. A user who asks on their laptop and clicks the link on their phone will get an error, not the reset page.
6. Test in a private window
Open your live site in a private window. Click “Forgot password?”, request the email, click the link, and check that you see the “choose a new password” form, can save it, and can log in with the new password. Supabase's built-in email service only sends 2 emails an hour, so if the email doesn't arrive, wait or set up your own SMTP.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with. Replace the parts in brackets first.
Our password reset link opens the home page instead of the reset page.
Live site: [https://yourapp.com]. Reset page should be: [/update-password].
The link in the reset email shows redirect_to=[paste what you see].
We use Supabase Auth with email and password.
Do only this:
1. Find the code that calls supabase.auth.resetPasswordForEmail. Make sure it passes
redirectTo: `${window.location.origin}/update-password` (no hardcoded localhost or preview URL).
2. Make sure a public /update-password page exists. On that page, listen to
supabase.auth.onAuthStateChange and show the "new password" form when the event is
PASSWORD_RECOVERY. Save it with supabase.auth.updateUser({ password }).
3. Find every redirect that sends signed-in users to the home page or dashboard
(route guards, SIGNED_IN handlers). Let /update-password through, so the form shows first.
4. If we use the PKCE flow (?code= in the URL or an /auth/confirm route), make sure the code
is exchanged once and the user is then sent to /update-password.
5. Tell me exactly what to put in Supabase > Authentication > URL Configuration:
the Site URL and the Redirect URLs for this domain (with and without www if both are live).
Don't change the login page design, sign-up, or the database.How to find out sooner next time
A broken reset is easy to miss. Users who can't reset their password usually just leave. Test it after any change to your domain, your Supabase URL settings, your email template, or your app's log-in code. AI edits to the log-in area often touch the redirect rules too.
Heedza's log-in check opens your live log-in page on a schedule. If the page links to a “Forgot password?” page, it checks that page's email field shows too, without sending it. If either page stops opening or loses its form, it sends you a screenshot and a fix prompt. Heedza never signs in, never submits the reset form and never opens reset emails, so it can't see where the reset link lands. Do the private-window test above yourself after changes.
Let Heedza check this for you. Heedza’s log-in check keeps opening your live log-in page and tells you if it stops opening or loses its form. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.