Lead Canary Logo
Lead Canary Logo
← Back to Blog

Stop Using Supabase Magic Links for Login

While magic links might seem like a great way to implement your login flow, they have a critical drawback that will make your users frustrated and eventually make them bounce. Here’s Why OTP Codes Are Better!

Alex Milman
Alex Milman
2024-08-305 min read
Stop Using Supabase Magic Links for Login

When I started building Lead Canary, a B2B lead generation platform, I wanted to make the login process as frictionless as possible. I wanted users to be able to log in from anywhere, at any time. I wanted to make it as easy as possible for users to get started. So I decided to forgo the traditional password login and opted for a magic link login instead.

At first, it seemed like a great idea. No more passwords to remember, no more passwords to reset. It was a breeze to implement and it worked great. At least I thought it did. A few days ago a user emailed me to let me know that they were unable to login. Now, my go to thought, being the developer I am, was that this guy didn't know what he was doing. After all, I've tested this code over and over again. Thankfully, I decided to take a closer look anyway.

What I've found is that magic links aren't so magic after all. They have a critical drawback that will baffle your users and cause them to bounce. If a user opens a magic link on a different device or different browser; The link will be invalid. Making your login UX a tedious nightmare.

>The Magic of Magic Links... Until They Aren't

Magic links have always seemed like the perfect solution: a frictionless, passwordless login experience where users simply click a link in their email to sign in. Sounds great, right? But there’s a catch, and a pretty significant one if you ask me.

// Example of implementing Supabase Magic Links
import { createClient } from '@supabase/supabase-js';

const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key');

async function sendMagicLink(email) {
  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: {
      emailRedirectTo: 'https://your-app-url.com/welcome',
    },
  });
  if (error) console.log('Error sending magic link:', error.message);
}

The magic quickly faded when I received an email from a Lead Canary user who was unable to log in. After some digging, I discovered the culprit: Supabase magic links. It turns out there’s a tricky little detail tucked away in the Supabase GitHub issues that threw a wrench in the whole experience.

According to Supabase’s GitHub, when using Magic Links in the PKCE (Proof Key for Code Exchange) flow, these links can only be opened from the same browser they were sent from. So, if a magic link was sent to your desktop’s Chrome browser, it would be completely invalid if you tried to open it on your mobile device.

Or even worse, if the user opens the link on the same device, but in a different browser, the link will also be invalid. For example, if you tried to sign in from a chrome browser on your mobile device, but your email opens the magic link in another default browser on the same device, the link will still be invalid 🤯.

Talk about a bummer! In today’s world, where we switch between devices more often than we change socks, this limitation can seriously mess with user experience.

>The Solution: OTP Codes to the Rescue!

After dealing with this frustrating issue, I knew I had to find a better solution. I'll be damned if I was going back to passwords, I needed a solution that would work on any device, in any browser and would relieve users from password fatigue. That’s when I decided to implement OTP (One-Time Password) codes instead. Thankfully, with Supabase, setting up OTP codes was a breeze, and it solved the problem beautifully.

// Example of implementing OTP with Supabase
import { createClient } from '@supabase/supabase-js';

const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key');

async function sendOtp(email) {
  const { error } = await supabase.auth.signInWithOtp({ email });
  if (error) console.log('Error sending OTP:', error.message);
}

async function verifyOtp(otp, email) {
  const { error, session } = await supabase.auth.verifyOtp({
    email,
    token: otp,
    type: 'email', // You can also specify 'sms' if using phone numbers
  });
  if (error) console.log('Error verifying OTP:', error.message);
  else console.log('Logged in with session:', session);
}

Here’s why OTP codes are the unsung heroes:

  1. Device Agnostic: OTP codes can be entered on any device, regardless of where the email was opened. No more worrying about which browser or device you used to request the login link.

  2. Let Go Of Email Confirmation: With OTP codes you don't need email confirmation. This means that users can sign up and login without the extra step of having to verify their email address.

  3. Security Boost: OTP codes are just as secure as magic links, if not more so. They’re time-sensitive and can only be used once, reducing the risk of unauthorized access.

  4. User-Friendly: Users appreciate not having to memorize passwords and switch devices or browsers just to log in. They can enter their OTP code right where they are, making the process smoother and faster.

  5. No More Confusion: Users won’t be scratching their heads wondering why their magic link isn’t working. OTP codes are straightforward and easy to understand.

>Wrapping It Up

Magic links might sound like a great idea on paper, but in practice, they can lead to frustrating experiences for your users—especially if they switch devices frequently. After my experience with Supabase’s magic link limitations, I made the switch to OTP codes, and I haven’t looked back since.

If you’re using Supabase (or any other service) and relying on magic links for login, I’d highly recommend considering OTP codes instead. Trust me, your users will thank you.