©Code byMilanParmar

Lovable password reset link not working after publishing

A symptom table to find your cause, then the Site URL, the redirect list, email scanners that eat the token, the built-in mailer's two-emails-an-hour cap, the reset page itself, what Lovable's built-in backend does differently, and a test to run before launch.

Updated 11 min read

A single steel chain link lying flat on a desk

A Lovable password reset link that opens localhost:3000means the Supabase Site URL was never changed from its default. A link that says it's invalid or expired usually means an email scanner opened it first. No email at all usually means the built-in mailer, which sends two emails an hour and only to your own team. Each has a five-minute fix.

Password reset is easy to skip when testing before launch, because it works in the Lovable preview and nobody forgets a password in week one. Then a real customer does, the email opens a page that doesn't exist, and they're locked out of something they paid for. This guide goes from what the customer saw to the setting that caused it. It's written for the founder, with enough detail for whoever helps them.

Which password reset problem do you have?

Ask the customer, or try it yourself on the live app with a real email address. What the link does when it's clicked tells you which cause to read.

Password reset symptoms in a Lovable app, their cause, and the fix
What happensUsual causeFix
Link opens http://localhost:3000Site URL never changed from the defaultSet the Site URL to the live domain (cause 1)
Link opens the home page, not the reset pageReset page not on the redirect URLs listAdd the exact page to the list (cause 2)
"Email link is invalid or has expired" on first clickA mail scanner opened the link first and used the tokenPoint the email at your own page with a token hash (cause 3)
Works for you, fails for customersBuilt-in mailer sends only to the project's teamSet up custom SMTP (cause 4)
No email, or only the first one or two arriveTwo emails an hour on the built-in mailerCustom SMTP, then raise the limit (cause 4)
Reset page loads, then bounces to loginThe app treats the recovery session like a normal loginHandle the recovery event on the reset page (cause 5)
Fixed yesterday, still broken todayTesting with an email sent before the fixRequest a fresh email after every change

Cause 1: the Site URL is still localhost:3000

Supabase builds every auth email around one setting, the Site URL. Its docs describe it as the default address users are sent to when the code doesn't name one, and say it's critical for email confirmations and password resets. The docs also tell you to change it from http://localhost:3000to your production address, because that's what a new project starts with. If nobody did, every reset email points at a developer's laptop.

This is common enough to have a GitHub issue. In October 2025 a user reported password reset and invite emails from a hosted Supabase project carrying redirect_to=http://localhost:3000, so nobody outside the office could finish a reset. The thread closed after they were pointed at the redirect URL guide, which is the same fix as here.

In your Supabase project, open Authentication, then URL Configuration, and set the Site URL to the address customers use, with the scheme and no trailing slash: https://app.example.com. Save it. Then request a new reset email, because emails sent before the change keep the old address inside the link. If your app runs on Lovable's built-in backend rather than your own Supabase project, Lovable sets this for you, as the section on the built-in backend explains. That's also the first setting to recheck after moving a Lovable app to your own hosting, because the domain changes and the setting doesn't.

Cause 2: the reset page isn't in the redirect URLs

Fixing the Site URL sends customers to your domain. It doesn't send them to the reset page. For that, the code passes a redirectToaddress when it requests the email, and Supabase's docs say that address should point at your change password page and should match the Redirect URLs list. If it isn't on the list, the customer lands on the Site URL instead, which is usually the home page, logged in with a session they can't use.

AI-generated code usually builds the address from the page it's running on, which is the right idea:

Requesting the reset email
const { error } = await supabase.auth.resetPasswordForEmail(email, {
  redirectTo: `${window.location.origin}/update-password`,
});

That works in the preview because the preview address is on the list. On your custom domain it fails until the domain is on the list too. Add the exact page, https://app.example.com/update-password, under Authentication, then URL Configuration, then Redirect URLs. Supabase supports wildcards: * matches one path segment, so https://app.example.com/* allows /update-password but not /auth/update, and ** matches everything after it. Its docs recommend the exact path in production and the wildcards for preview deployments, for example https://*-yourteam.vercel.app/**. If you use Lovable's own preview URL and a custom domain, both need an entry. If it isn't just the reset email but every sign-in that breaks on the custom domain, including Google, start with Lovable login not working on a custom domain, which covers the www and Google Cloud entries too.

Cause 3: email link is invalid or has expired

The full message is usually Email link is invalid or has expired, sometimes with error_code=otp_expiredin the address bar, and the customer swears they clicked within a minute. Supabase's troubleshooting page names the most common reason: email prefetching. Security tools at the customer's email provider open links in incoming mail to check them for malware. A password reset link is single-use, so the scanner uses it, and the real click arrives second. Business inboxes with link scanning switched on do this, which is why it hits business customers and not your Gmail test.

The default reset email uses {{ .ConfirmationURL }}, which verifies the token on the same request that opens it. Supabase's email template docs warn that this link gets consumed instantly by a prefetching provider and produces the expired error. Two fixes, and the docs describe both.

Point the email at your own page

Change the reset template so the link carries the token hash instead of confirming on open. In Authentication, then Email Templates, edit Reset Password:

Reset Password template link
<a href="{{ .SiteURL }}/update-password?token_hash={{ .TokenHash }}&type=recovery">
  Reset your password
</a>

Nothing happens when a scanner opens that page. The token is used only when the customer submits a new password, and the page does the verification itself:

On the update-password page
const params = new URLSearchParams(window.location.search);
const { error } = await supabase.auth.verifyOtp({
  token_hash: params.get("token_hash")!,
  type: "recovery",
});
if (!error) {
  await supabase.auth.updateUser({ password: newPassword });
}

Or send a code instead of a link

The template docs also offer {{ .Token }}, a short code the customer types into your app. Nothing in the email is clickable, so nothing can be prefetched. It's one more step for the customer, and it's the option the troubleshooting page suggests when prefetching keeps happening.

Supabase also recommends turning off any email tracking at your mail provider, because tracking rewrites every link in the email and the rewritten reset link doesn't work.

Cause 4: no password reset email arrives

A new Supabase project sends auth emails through Supabase's own mail service, and its docs are blunt about what that service is for: testing and non-critical projects, with no delivery guarantee. Two limits catch Lovable apps after launch.

  • It only delivers to your team.The custom SMTP guide says the default service refuses addresses that aren't members of the project's team, with an "Email address not authorized" error. That's why resets work for you and fail for every customer.
  • Two emails an hour.The rate limits page gives the built-in provider's limit as 2 emails per hour for the whole project. Sign-up confirmations count toward it. On launch day, the third person who signs up gets nothing, and the sign-in form starts answering with Email not confirmed or Email rate limit exceeded.
  • One request a minute per user.The same page says a password reset request for the same user defaults to a 60-second window. A customer who clicks "send again" straight away gets no second email, and your app should say so instead of failing quietly.

The fix is custom SMTP. Supabase's guide walks through it: create an account with a sending service (it names Resend, Postmark, AWS SES, SendGrid, Brevo, and ZeptoMail), take the host, port, username, and password, and enter them under Authentication, then SMTP settings. Once saved, the project sends to every address, and the limit starts at 30 messages an hour, which you raise on the Rate Limits page. Set up the sending domain's SPF, DKIM, and DMARC records with the provider, or the reset emails land in spam, which looks exactly like "no email" to the customer. This is one of the checks on the free vibe-coded app launch checklist.

Cause 5: the reset page doesn't finish the job

Sometimes the email is right, the link is right, and the customer still can't set a password. When they click, Supabase gives the browser a temporary session and sends it to your reset page. If the app isn't expecting that, it does what it does for any session: redirects to the dashboard, or to login, and the chance to change the password is gone. On the second click the link is used up.

Supabase's reference example handles this with the auth listener. When the event is PASSWORD_RECOVERY, the page should show the new password form and nothing else, then call updateUser:

Reset page, implicit flow
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === "PASSWORD_RECOVERY") {
    setShowForm(true); // don't redirect, don't treat it as a login
  }
});

async function submit(newPassword: string) {
  const { error } = await supabase.auth.updateUser({ password: newPassword });
  if (!error) router.replace("/login");
}

Two things break this in generated code. A route guard that sends any signed-in user away from public pages fires before the form renders. And a redirect inside the same auth listener runs on the recovery event too. Put the reset page outside the guard, and make the listener do nothing on PASSWORD_RECOVERY except show the form. If you switched the template to a token hash in cause 3, the page verifies the token itself and this event never fires, which is simpler. The same listener is where the login hang in a Lovable app that hangs after login comes from, so fix both at once.

Lovable's built-in backend vs your own Supabase

Which fixes apply depends on where your app's backend lives. Lovable's docs say auth settings are configured through Lovable on the built-in backend, and in the Supabase dashboard when the app uses your own Supabase project, and that switching between the two isn't automatic.

On the built-in backend

Lovable's authentication docs say it sets the Site URL to your published lovable.appaddress when you publish, and adds your custom domain to the redirect list automatically. A Site URL you set yourself stays as you set it. So causes 1 and 2 are mostly handled for you, unless you overrode them. The reset email itself is customised under More, then Emails, and Lovable's email auth page says a per-hour cap on auth emails sits under Advanced in More, then Users, then Settings, then Email, and can be raised once email sending is set up for the project. Lovable's docs don't say whether the built-in backend exposes Supabase's SMTP or email template settings directly, so if you need the token hash change from cause 3, check the Emails view first, and ask Lovable support if it isn't there.

On your own Supabase project

Everything in this guide applies, nothing is automatic, and all of it lives in your Supabase dashboard under Authentication. Lovable only generates the code that calls it. That's the setup most apps with real customers end up on, and it's the one where the two-emails-an-hour default bites hardest, because nobody told you it was there.

Test the password reset flow before launch

Login problems were one of the things wrong with the social challenge appwhen I took it over from an agency, alongside payments and security, on a product that moves real money. The fix for a reset flow is small. Finding out from a locked-out customer is not. Run this on the live domain, not the preview, with an inbox that isn't on the Supabase team:

  1. Request a reset for a Gmail address and for a business address with link scanning on, such as a company Microsoft 365 inbox. The second one catches prefetching.
  2. Open the email on a phone, in the mail app's built-in browser. Confirm it lands on the reset page, not the home page or login.
  3. Set a new password, then log in with it on a different device.
  4. Click the same link again. It should say it's expired, with a clear way to request a new one.
  5. Request three resets inside an hour from three addresses. If the third never arrives, you're still on the built-in mailer.
  6. Check the spam folder for every email above. If any landed there, fix SPF, DKIM, and DMARC before launch.

When to get help with Lovable password reset

If the symptom table pointed at one cause and the fix took ten minutes, you're done. Get help when the reset is one of several auth problems, when customers report being able to see things they shouldn't after a reset, or when the app is about to take payments and you haven't tested any of the login flows end to end. A broken reset and a leaky database often arrive together, because both come from settings nobody opened. The Supabase RLS checklist covers the second one.

Logins and auth are one of the six areas in my Launch-Ready Review, a fixed-price check of a vibe-coded app for $900–1,500 in three to five days, with a written report and a priced fix list. If the list is long, fixing it is a Production Sprint for $3,000–7,500. What a one-off fix costs instead is in what it costs to fix a vibe-coded app. Either way, send me the app's linkand a screenshot of what the reset email opens, and I'll tell you which cause it is.

Sources

  1. Redirect URLs: Site URL default, wildcards, and Netlify and Vercel previews, Supabase Docs
  2. Password-based auth: resetting a password with resetPasswordForEmail, Supabase Docs
  3. resetPasswordForEmail reference, with the PASSWORD_RECOVERY example, Supabase Docs
  4. OTP verification failures: email prefetching consumes the token, Supabase Docs
  5. Email templates: link prefetching, the Token variable, and the TokenHash variable, Supabase Docs
  6. Auth rate limits: 2 emails an hour on the built-in provider, 60 seconds per user, Supabase Docs
  7. Custom SMTP: the default service sends only to team members, 2 messages an hour, Supabase Docs
  8. Password reset and invite links use localhost:3000 in production (issue #39718), GitHub, supabase/supabase
  9. Authentication: Site URL and redirect URLs on the built-in backend, Lovable Docs
  10. Email auth: reset emails, the per-hour cap, and where the settings live, Lovable Docs
  11. Supabase integration: auth settings live in your Supabase project, Lovable Docs
Milan Parmar

The developer

Freelance full-stack developer in Bengaluru with 5+ years of experience. I redesign websites, take over broken apps, and build SaaS products, with 18 case studies to show for it.

FAQs

Why does the reset link still open localhost after I changed the Site URL?

Because the email was built before you changed it. Supabase puts the redirect address into the link when it sends the email, so an email sent earlier keeps the old address forever. Request a new reset email after saving the settings, and check that one. If a fresh email still opens localhost, the reset page isn't on the redirect list, or the app's redirectTo value is wrong.

Does any of this affect Google or GitHub login too?

The Site URL and redirect list do. Social logins return users to an address after they sign in with the provider, and that address has to be allowed in the same list, or users land on the Site URL instead. The email causes don't apply, because no email is sent. If social login works and only the reset email fails, start with the email scanner and the mailer limits.

Why does the reset link work on desktop but not from my phone's mail app?

Usually the link is being opened inside the mail app's own browser, which has no session and sometimes drops the part of the address after the hash. The token still gets used, so a second tap says the link is expired. Pointing the email at your own page with a token_hash, as described above, fixes this, because the page verifies the token itself instead of relying on what the browser kept.

How long is a Supabase password reset link valid?

Supabase's troubleshooting page says the user must act within a set timeframe but doesn't publish a fixed number on that page, because the expiry is a setting on your project. You change it in the auth settings under email. Keep it short enough that a forwarded email isn't a risk and long enough for a slow mail server, then test with a real inbox rather than guessing.