Supabase Magic Link: Passwordless Sign-In, and Why It Breaks on Mobile

Entrig
Ib Entrig Team

Magic links are the nicest login flow to demo and one of the more awkward ones to ship, especially on mobile. This covers the setup, the two places people reliably get stuck, and the case for sending a one-time code instead, which is the option most mobile apps should probably be using.

If your question is about where the emails come from and how they look rather than how the flow works, that is covered in Supabase Auth Emails: Custom SMTP, Templates, and Taking Them Over.


The Basic Flow

Supabase’s magic link is one call:

const { error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://yourapp.com/auth/callback',
  },
})

That generates a single-use token, emails it, and creates the user if they do not already exist. If you do not want signup-by-login, pass shouldCreateUser: false and handle the “no such user” case yourself.

When the recipient opens the link, they hit Supabase’s verify endpoint:

{SUPABASE_URL}/auth/v1/verify?token={token_hash}&type=magiclink&redirect_to={redirect_to}

Supabase validates the token and redirects to your redirect_to with the session attached. On the web, supabase-js picks it up automatically if detectSessionInUrl is on, or you call exchangeCodeForSession when using the PKCE flow.

One thing to get right immediately: every URL you pass as emailRedirectTo has to be listed under Authentication → URL Configuration → Redirect URLs in the Supabase dashboard. If it is not on that allowlist, Supabase silently falls back to your Site URL, and you get a working login that lands on the wrong page with no error explaining why. That is the single most common magic link support question.


Where It Breaks on Mobile

Here is the part that catches people out, and it is not a bug. It is what magic links fundamentally are.

The link opens in the device’s default browser, not in your app.

The user is in your Flutter or React Native app. They type their email, tap the button, switch to Gmail, tap the link, and iOS or Android hands that URL to Safari or Chrome. Supabase verifies the token there, in the browser, and redirects to your URL. Unless that URL is a deep link your app has registered, the sign-in completes inside a browser tab and your app never hears about it.

The user is now signed in somewhere they are not looking, and signed out in the app they are actually using.

Making it work means all of the following:

  1. Register a deep link scheme in the app. A custom scheme like yourapp://login-callback, or a universal link (iOS) and App Link (Android) if you want it to work without a scheme prompt.
  2. Add that URL to Supabase’s Redirect URLs allowlist, exactly, including the scheme.
  3. Pass it as emailRedirectTo on the signInWithOtp call.
  4. Handle the incoming link in the app and pass it to supabase-js so the session is stored on the right side.
  5. Use the PKCE flow, which is the default in recent supabase-js versions for good reason, and exchange the code for a session once the deep link arrives.

Each step is documented and none of it is hard. But it is five moving parts across two platforms plus a dashboard allowlist, and every one of them fails silently. Universal links in particular are a well-known source of misery: they need a hosted apple-app-site-association file, they cache aggressively, and they behave differently depending on which app the user tapped the link from.

There is also a failure mode you cannot engineer around. Some email clients open links in an in-app browser (Gmail’s built-in viewer does this), which may not honour your universal link at all. The user taps, something happens, and they are not signed in.


The Better Mobile Answer: A One-Time Code

Here is the thing most people do not realise: the same signInWithOtp call already generates a six digit code. You do not need a different API, a different flow, or a different plan.

Supabase issues both a link token and a numeric token on every magic link request. Verifying the code is one call:

const { data, error } = await supabase.auth.verifyOtp({
  email: 'user@example.com',
  token: '123456',
  type: 'email',
})

Look at what that removes:

  • No deep link scheme to register
  • No universal link or App Link configuration
  • No apple-app-site-association file to host
  • No redirect URL allowlist to keep in sync
  • No browser handoff at all
  • No in-app-browser edge cases

The user never leaves your app. They read six digits, type them into a field you control, and they are signed in. It is the flow every bank, Slack, and Discord app uses on mobile, and it is not a coincidence.

Magic links are a web pattern. On the web they are excellent, because the browser that opens the link is the same context the user started in. On mobile, that assumption is false, and everything downstream of it is workaround.


So why does everyone use links on mobile anyway? Because of what is in the email.

Supabase’s default magic link template renders {{ .ConfirmationURL }} and nothing else. The six digit token exists in the template context as {{ .Token }}, but the shipped template does not use it. If you want your users to receive a code, you have to go into Authentication → Email Templates, find the magic link template, and hand-edit its HTML.

That is fine once. It gets tedious when you realise:

  • Each auth email type is a separate template with its own HTML
  • Your branding lives inline in each of them, so a logo change is a seven-file edit
  • There is no preview beyond sending yourself a real email
  • Switching from link to code later means editing HTML again, per type
  • If you want both a link and a code in the same email, you are writing that layout yourself

None of this is hard. It is just fiddly work that sits between you and trying a different login flow, which is why most projects ship whatever the default template did.


Where Entrig Fits

Entrig takes over your project’s auth emails through Supabase’s Send Email Hook, and one of the things that becomes configuration instead of HTML is exactly this decision.

For each auth email type, including magic link, you pick link, code, or both, from the dashboard. No template editing.

That makes the trade-off above something you can actually test. Ship the code flow to see if your mobile signup completion improves, switch back in one click if it does not, and run link on web while running code on mobile if that is what your users need. Both is a reasonable default for a magic link email, since the same message then works whichever context the recipient opens it in.

A few related things come with it:

  • Branding is applied at send time, so your logo, wordmark and accent colour apply to every future email, retroactively. It is not copied into each template.
  • The expiry copy is real. Every email that hands out something time-limited states how long it lasts, read from your project’s own Email OTP Expiration value rather than a hardcoded guess. A user told “24 hours” who returns to a dead link has no way to understand what happened.
  • Expiry is editable from the same screen, and writes straight back to your Supabase project, so it is one value whether you change it here or there.

Being straight about the boundaries:

  • This changes what the email contains, not how Supabase Auth works. signInWithOtp and verifyOtp are unchanged, and the deep link work is still required if you choose the link flow.
  • Reauthentication is always a code, never a link, because Supabase gives that type no verification URL to work with.
  • You still need a verified sending domain.

Setup is in the Auth Email guide.


Gotchas Worth Knowing

Testing with one address. Requesting several links in a row with your own address is an awkward way to test, because each new token invalidates your last one and you lose track of which email you are looking at. Use plus-addressing (you+1@yourapp.com, you+2@yourapp.com), which most providers deliver to the same inbox while Supabase treats them as separate users.

Expiry defaults. Most projects sit at one hour. Supabase’s security advisor flags anything longer. Whatever you choose, make sure the email states the true number.

Single use means single use. Some corporate mail scanners and link previewers fetch URLs in emails before the human ever clicks. That consumes the token, and your user gets an “invalid or expired link” on their first tap. This is another argument for codes in environments with aggressive email security, since a scanner cannot accidentally type six digits into your app.


Summary

Magic linkOne-time code
WebExcellentFine
MobileNeeds deep links, universal links, PKCEWorks with no setup
Breaks on in-app browsersYesNo
Killed by link scannersYesNo
User leaves your appYesNo

If you are building a mobile app on Supabase, try the code flow. It is the same API call you are already making, and the only real thing standing between you and testing it is what your email template happens to render.


Frequently asked questions

How do I send a magic link with Supabase?

Call supabase.auth.signInWithOtp({ email }) from your client. Supabase generates a single-use token, emails it to the address, and creates the user if they do not already exist. When the recipient opens the link, Supabase verifies the token and redirects to your configured redirect URL with a session.

Why does my Supabase magic link not work in my mobile app?

The link opens in the device's default browser, not your app. Unless you have configured a deep link redirect URL and registered the matching custom scheme or universal link in the app, the browser completes the sign-in and your app never receives the session. The user ends up signed in inside a browser tab and still signed out in the app.

Can Supabase send a one-time code instead of a magic link?

Yes. The same signInWithOtp call generates both a link and a six digit token. Supabase's default email template only renders the link, so to show the code you either edit the template HTML to include the token variable, or use a Send Email Hook service that lets you pick link, code, or both per email type.

Should I use a magic link or a one-time code in a mobile app?

A code is usually better on mobile. It needs no deep linking, no universal link configuration and no browser handoff, and it keeps the user inside your app the whole time. Magic links are a better fit on the web, where the browser that opens the link is the same context the user started in.

How long does a Supabase magic link last?

It comes from your project's Email OTP Expiration setting, which most projects have at one hour. Supabase's security advisor flags anything above one hour. Whatever you set, the email should state the real value, because a user told twenty four hours who comes back to a dead link has no idea what went wrong.

Can I send both a link and a code in the same email?

Not with Supabase's default template, which renders only the confirmation URL. You can hand-edit the template HTML to include both the link and the token variable, or use a Send Email Hook service that offers link, code, or both as a per-type setting.