Supabase Auth in Production: Redirects, Email and Sessions

  • Wednesday, October 21, 2026

Supabase Auth in production: fix redirect URLs, confirmation emails, OAuth setup and session settings before real users try to sign in.

Most Supabase Auth problems in production come from configuration rather than code: a Site URL still pointing at localhost, a redirect allow list that does not include your domain, a built-in email service that only delivers to your own team, and session settings nobody has opened. Each one takes minutes to fix once you know which is which.

This guide is for founders and small teams taking a Supabase app live, including apps generated with Lovable, Bolt or Cursor, where sign-in worked perfectly on a laptop and fails for the first real user. It covers redirects, confirmation emails, OAuth providers and sessions, and names the setting to change in each case.

Key takeaways

  • Site URL is the default destination for every auth email. It ships as http://localhost:3000 and has to be changed to your production address.
  • The built-in email service is for testing. It refuses addresses outside your organisation and sends about two messages an hour.
  • "Token has expired or is invalid" is often an email scanner opening the one-time link before your user does.
  • OAuth needs two settings to agree: the provider's authorised redirect URI and your Supabase redirect allow list.
  • Sessions never end by default. Inactivity timeouts, time-boxing and single-session enforcement are opt-in and only on paid plans.

Why does Supabase Auth break in production?

Because a new project ships with development defaults and nothing warns you they are still in place. The authentication code generated for you is usually fine. The environment around it is not.

These are the failures we see most often when a Supabase app meets its first real users, and where each one is fixed.

Symptom Usual cause Where to fix it
Confirmation link opens localhost Site URL never changed URL configuration
Redirect lands on the wrong page redirectTo not on the allow list URL configuration
Only your own team gets emails Built-in email service still in use SMTP settings
"Token has expired or is invalid" A scanner opened the link first Email template
Google returns a redirect URI error Callback URL missing in the provider Google Cloud console
Users signed out at random Two clients refreshing one session Client setup

Signing a user in is a separate job from deciding what they can read. Our guide to writing row-level security policies covers the half of this that lives in the database, and it is the half that leaks data when it is missing.

How do you fix the Site URL and redirect URLs?

Set Site URL to your production address, then add every other legitimate destination to the redirect allow list. Supabase's documentation states that Site URL defaults to http://localhost:3000 and that this setting is critical for email confirmations and password resets (Supabase redirect URL documentation).

The two settings do different jobs. Site URL is the fallback destination when your code does not pass a redirectTo value, and it is what email templates fall back to. The allow list is the set of addresses Supabase agrees to send a user to when your code does pass one.

When authentication fails, the user is still sent to the redirect URL, with the failure described in the URL fragment. Reading error_code and error_description on that page turns "it bounced me back to the login screen" into a message you can act on.

Preview deployments and wildcards

Preview URLs change on every deploy, so the allow list accepts wildcard patterns, where . and / count as separators. For a Vercel project, Supabase suggests keeping Site URL on the production domain and adding entries such as http://localhost:3000/** and https://*-your-team.vercel.app/**.

Keep the globstar for development and preview environments only. The same documentation recommends an exact path for the production redirect, which also keeps the allow list readable a year from now.

Why don't confirmation emails arrive?

Because the project is almost certainly still using Supabase's built-in email service. It only delivers to members of your organisation, and Supabase describes it as best-effort, not intended for production, currently limited to two messages per hour; other addresses fail with "Email address not authorized" (Supabase custom SMTP documentation).

That is why sign-up works for the founder and nobody else. Your address is on the team, so your own test passes.

  Built-in email service Custom SMTP
Who can receive mail Organisation members only Anyone
Throughput About 2 messages per hour 30 per hour, adjustable
Delivery guarantee Best effort, no SLA Your provider's
Sender address Supabase's Your own domain
Intended for Demos and template testing Production

Switching to custom SMTP

Any provider that speaks SMTP works, and Supabase names Resend, AWS SES, Postmark, Twilio SendGrid, ZeptoMail and Brevo among them. Once it is enabled, delivery opens up to real addresses with a starting limit of 30 messages per hour, which you raise in the rate-limit settings as your sending domain earns a reputation.

Sending is not the same as arriving. A password reset from a brand-new domain with no authentication records gets filtered no matter how well the template is written, and the DNS side of that is covered in our walkthrough of SPF, DKIM and DMARC for app email.

Why do confirmation links say the token expired?

Usually because something opened the link before your user did. Corporate mail security, including Safe Links in Microsoft Defender for Office 365, prefetches URLs in incoming messages, which consumes a single-use confirmation link and leaves the real click with "Token has expired or is invalid" (Supabase email template documentation).

Three fixes, in the order we usually try them:

  1. Send a code instead of a link. Put {{ .Token }} in the template, ask the user to type the six digits, and verify them with verifyOtp. A scanner has nothing to consume.
  2. Add a confirmation page. Link to a page on your own site that carries the confirmation URL as a parameter, and make the user press a button there.
  3. Verify on your server. Point the template at your own endpoint with the token hash, exchange it for a session server-side, then redirect the user into the app.

The third option is the one to reach for in a server-rendered app, because the default link returns the session in a URL fragment that your server never sees. The template link looks like this:

<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">
  Confirm your email address
</a>

And the route behind it exchanges the hash for a session:

const { data, error } = await supabase.auth.verifyOtp({
  token_hash,
  type: 'email',
})

One more setting worth checking in your email provider: click tracking rewrites every link in a message, which breaks auth links in particular. Turn it off for transactional mail.

What does OAuth need in production?

Two sets of configuration that have to agree with each other. In the provider's console your application's domain goes in the authorised JavaScript origins and your Supabase project's callback URL goes in the authorised redirect URIs; in Supabase, whatever your code passes as redirectTo has to be on the allow list (Supabase guide to Sign in with Google).

Two details catch teams out at launch.

  • The consent screen shows your project reference unless you set a custom domain. Supabase suggests something like auth.example.com for an app on example.com, and points out that the default makes a phishing page harder for users to spot.
  • Provider tokens are not stored or refreshed for you. If your product calls Google or Slack APIs on a user's behalf, keeping those tokens alive is your job, on a server you control.

Server-rendered apps also need a callback route. The PKCE flow, which is the exchange used when the session has to be created on a server rather than in the browser, returns a code that your route swaps for a session before the user lands on a page.

How long do Supabase sessions last?

By default, until the user signs out, on as many devices as they like. The access token is a JWT that expires after an hour by default, while the refresh token behind it never expires but can only be used once (Supabase session documentation).

That single-use rule has a ten-second reuse window, which exists so a server and a browser can hand the same token around without ending the session. Outside that window, a reused token is treated as a compromised session and every refresh token for it is revoked.

This is behind most reports of users being logged out for no reason. The usual cause is two clients refreshing the same session at once, which is easy to do when a server-rendered app also creates a browser client that manages its own refresh.

When should you shorten a session?

When a customer's security review, an auditor or a shared-device workflow asks for it. Inactivity timeouts, time-boxed sessions and single-session-per-user are configurable on Pro plans and above, and they take effect at the next refresh rather than immediately. If a buyer's security questionnaire started this conversation, our broader guide to sessions, roles and SSO in a SaaS product covers what they usually ask for next.

Two numbers matter when you set them. The effective session length is your timeout plus the access token lifetime, because the check happens when the token is refreshed. And Supabase advises against access token lifetimes under five minutes, since clock skew on user devices produces failures that are painful to debug.

Common mistakes

  • Updating Site URL but leaving the old preview domain as the only entry on the allow list.
  • Launching on the built-in email service and reading "Email address not authorized" as a bug in the sign-up form.
  • Leaving click tracking on for the provider that sends your auth email.
  • Testing sign-in only in the browser profile where you are already signed in, which hides a broken redirect.
  • Treating a successful login as access control. The database decides what that user can read.

Frequently asked questions

Why does my magic link still redirect to localhost?

Either Site URL is still set to the development address, or the template uses {{ .SiteURL }} while your code passes a redirectTo value. Switching the template to {{ .RedirectTo }} fixes the second case, and both need the destination on the allow list.

Do I need custom SMTP if I am on a paid plan?

Yes. The restriction belongs to the built-in email service rather than to the plan, so a Pro project still cannot email addresses outside the organisation until SMTP is configured. Treat it as a launch requirement alongside a custom domain.

Can I keep users signed in for 30 days?

That is already the default behaviour, since sessions last until sign-out unless you configure a timeout. If you want exactly 30 days, set a time-box of 30 days on a Pro plan and leave the access token lifetime at an hour.

Why is the browser signed in while my server routes see an anonymous user?

Because the session lives in browser storage, where server code cannot read it. Server-rendered apps need the session in cookies, which is what Supabase's server-side helpers do, plus a callback route that completes the code exchange before rendering.

Before the first real sign-up

Work through the list in order: Site URL and the allow list, custom SMTP, an email template that survives link scanners, provider callback URLs, and a session policy you have chosen on purpose. None of it is difficult, and all of it stays invisible until a stranger tries to sign in. If the app also behaves differently on its live domain than in preview, our list of nine gaps between a preview and a published app covers the rest.

If you would rather hand the login, email and session setup to senior engineers, AlgoSmiths works on fixed-price projects with a working demo every week. Our SaaS product engineering work is where auth, billing and onboarding get built together.

Posted In:
Software & SaaS Solutions

Add Comment Your email address will not be published