To enable magic link login in Supabase, go to Authentication > Providers > Email in the Dashboard and ensure the email provider is enabled. Then call supabase.auth.signInWithOtp({ email }) in your client code. Supabase sends a magic link to the user's email, and clicking it signs them in without a password. Handle the redirect by setting emailRedirectTo in the options and processing the auth callback in your application.
Enabling Passwordless Magic Link Login in Supabase
Magic link login lets users sign in by clicking a link sent to their email — no password required. It reduces friction for users and eliminates password-related support issues. Supabase supports magic links out of the box through the signInWithOtp method. This tutorial walks you through enabling the email provider, implementing the client-side code, handling the redirect after the user clicks the link, and configuring SMTP for production use.
Prerequisites
- A Supabase project with Auth enabled
- @supabase/supabase-js v2+ installed in your frontend project
- A frontend framework (React, Next.js, Vue, etc.) with routing
- Custom SMTP configured (recommended for production to avoid 2 emails/hour limit)
Step-by-step guide
Enable the email provider in the Dashboard
Enable the email provider in the Dashboard
Go to the Supabase Dashboard, navigate to Authentication > Providers > Email. Make sure the email provider is enabled. Magic links work through the email provider — there is no separate toggle for magic links. By default, the email provider is enabled when you create a new Supabase project. Optionally, if you want users to sign in exclusively via magic link without passwords, you can disable 'Allow new users to sign up' and only use signInWithOtp, which creates the user and signs them in with a single call.
Expected result: The email provider is enabled in the Supabase Dashboard under Authentication > Providers.
Implement magic link login in the client
Implement magic link login in the client
Use supabase.auth.signInWithOtp() with the user's email address. This sends a magic link email to the user. If the user does not have an account yet, Supabase automatically creates one (unless signups are disabled). Set the emailRedirectTo option to tell Supabase where to send the user after they click the magic link. This should be a page in your application that handles the auth callback.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78async function sendMagicLink(email: string) {9 const { data, error } = await supabase.auth.signInWithOtp({10 email,11 options: {12 emailRedirectTo: 'https://your-app.com/auth/callback',13 // Set to false to prevent creating new accounts14 // shouldCreateUser: false,15 },16 })1718 if (error) {19 console.error('Error sending magic link:', error.message)20 return { success: false, error: error.message }21 }2223 return { success: true, message: 'Check your email for the magic link!' }24}Expected result: Calling sendMagicLink with an email address sends a magic link email to the user.
Handle the magic link redirect callback
Handle the magic link redirect callback
When the user clicks the magic link in their email, Supabase redirects them to your emailRedirectTo URL with auth parameters in the URL hash. Your application needs to handle this redirect by extracting the session from the URL. The Supabase client does this automatically when the page loads if you have an onAuthStateChange listener set up. For Next.js App Router, create a route handler that exchanges the code for a session.
1// For Next.js App Router: src/app/auth/callback/route.ts2import { createServerClient } from '@supabase/ssr'3import { cookies } from 'next/headers'4import { NextResponse } from 'next/server'56export async function GET(request: Request) {7 const { searchParams, origin } = new URL(request.url)8 const code = searchParams.get('code')9 const next = searchParams.get('next') ?? '/dashboard'1011 if (code) {12 const cookieStore = await cookies()13 const supabase = createServerClient(14 process.env.NEXT_PUBLIC_SUPABASE_URL!,15 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,16 {17 cookies: {18 getAll() { return cookieStore.getAll() },19 setAll(cookiesToSet) {20 cookiesToSet.forEach(({ name, value, options }) =>21 cookieStore.set(name, value, options)22 )23 },24 },25 }26 )2728 const { error } = await supabase.auth.exchangeCodeForSession(code)29 if (!error) {30 return NextResponse.redirect(`${origin}${next}`)31 }32 }3334 return NextResponse.redirect(`${origin}/login?error=auth_failed`)35}Expected result: Users are redirected to your app and signed in after clicking the magic link in their email.
Build the magic link login form
Build the magic link login form
Create a simple form that collects the user's email address and shows feedback after the magic link is sent. The form should show a success message telling the user to check their email, and handle error states like rate limiting or invalid email addresses. No password field is needed since magic links are passwordless.
1// React component: MagicLinkLogin.tsx2import { useState } from 'react'3import { createClient } from '@supabase/supabase-js'45const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!8)910export function MagicLinkLogin() {11 const [email, setEmail] = useState('')12 const [status, setStatus] = useState<'idle' | 'loading' | 'sent' | 'error'>('idle')13 const [message, setMessage] = useState('')1415 async function handleSubmit(e: React.FormEvent) {16 e.preventDefault()17 setStatus('loading')1819 const { error } = await supabase.auth.signInWithOtp({20 email,21 options: {22 emailRedirectTo: `${window.location.origin}/auth/callback`,23 },24 })2526 if (error) {27 setStatus('error')28 setMessage(error.message)29 } else {30 setStatus('sent')31 setMessage('Check your email for a login link!')32 }33 }3435 if (status === 'sent') {36 return <p>{message}</p>37 }3839 return (40 <form onSubmit={handleSubmit}>41 <input42 type="email"43 value={email}44 onChange={(e) => setEmail(e.target.value)}45 placeholder="Enter your email"46 required47 />48 <button type="submit" disabled={status === 'loading'}>49 {status === 'loading' ? 'Sending...' : 'Send magic link'}50 </button>51 {status === 'error' && <p style={{ color: 'red' }}>{message}</p>}52 </form>53 )54}Expected result: A login form that collects an email address, sends a magic link, and shows appropriate feedback.
Configure custom SMTP for reliable delivery
Configure custom SMTP for reliable delivery
The default Supabase SMTP has a limit of 2 emails per hour, which is not suitable for production. Configure a custom SMTP provider to remove this limit and improve email deliverability. Go to Dashboard > Authentication > SMTP Settings and enter your SMTP credentials. Popular options include Resend (recommended for developers), SendGrid, Mailgun, and Amazon SES. After configuring, test by sending a magic link to ensure emails arrive quickly.
1# SMTP Settings in Dashboard > Authentication > SMTP Settings:2#3# Sender email: noreply@your-domain.com4# Sender name: Your App Name5# Host: smtp.resend.com (example for Resend)6# Port: 4657# Username: resend8# Password: re_your_api_key9#10# Other popular SMTP providers:11# SendGrid: smtp.sendgrid.net, port 58712# Mailgun: smtp.mailgun.org, port 58713# Amazon SES: email-smtp.us-east-1.amazonaws.com, port 587Expected result: Custom SMTP is configured and magic link emails are delivered reliably without the 2 emails/hour limit.
Complete working example
1// Complete Magic Link Authentication Implementation2import { useState, useEffect } from 'react'3import { createClient, Session } from '@supabase/supabase-js'45const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!8)910export function MagicLinkAuth() {11 const [email, setEmail] = useState('')12 const [status, setStatus] = useState<'idle' | 'loading' | 'sent' | 'error'>('idle')13 const [message, setMessage] = useState('')14 const [session, setSession] = useState<Session | null>(null)1516 useEffect(() => {17 // Check for existing session18 supabase.auth.getSession().then(({ data: { session } }) => {19 setSession(session)20 })2122 // Listen for auth state changes (including magic link callback)23 const { data: { subscription } } = supabase.auth.onAuthStateChange(24 (_event, session) => {25 setSession(session)26 }27 )2829 return () => subscription.unsubscribe()30 }, [])3132 async function handleSendMagicLink(e: React.FormEvent) {33 e.preventDefault()34 setStatus('loading')3536 const { error } = await supabase.auth.signInWithOtp({37 email,38 options: {39 emailRedirectTo: `${window.location.origin}/auth/callback`,40 },41 })4243 if (error) {44 setStatus('error')45 setMessage(error.message)46 } else {47 setStatus('sent')48 setMessage('Check your email for a login link!')49 }50 }5152 async function handleSignOut() {53 await supabase.auth.signOut()54 setSession(null)55 }5657 // User is signed in58 if (session) {59 return (60 <div>61 <p>Signed in as {session.user.email}</p>62 <button onClick={handleSignOut}>Sign out</button>63 </div>64 )65 }6667 // Magic link sent68 if (status === 'sent') {69 return (70 <div>71 <p>{message}</p>72 <button onClick={() => setStatus('idle')}>Try another email</button>73 </div>74 )75 }7677 // Login form78 return (79 <form onSubmit={handleSendMagicLink}>80 <h2>Sign in with magic link</h2>81 <input82 type="email"83 value={email}84 onChange={(e) => setEmail(e.target.value)}85 placeholder="your@email.com"86 required87 />88 <button type="submit" disabled={status === 'loading'}>89 {status === 'loading' ? 'Sending...' : 'Send magic link'}90 </button>91 {status === 'error' && <p style={{ color: 'red' }}>{message}</p>}92 </form>93 )94}Common mistakes when enabling Magic Link Login in Supabase
Why it's a problem: Not setting emailRedirectTo, causing the magic link to redirect to the default Supabase URL instead of your application
How to avoid: Always set the emailRedirectTo option in signInWithOtp to your application's callback URL. Add this URL to the Redirect URLs allowlist in Dashboard > Authentication > URL Configuration.
Why it's a problem: Using the default SMTP in production and hitting the 2 emails/hour rate limit
How to avoid: Configure a custom SMTP provider (Resend, SendGrid, Mailgun) in Dashboard > Authentication > SMTP Settings before going to production.
Why it's a problem: Not adding the redirect URL to the allowlist in the Supabase Dashboard
How to avoid: Go to Dashboard > Authentication > URL Configuration and add your callback URL (e.g., https://your-app.com/auth/callback) to the Redirect URLs list.
Why it's a problem: Not handling the auth callback on the redirect page, leaving the user in a loading state
How to avoid: For Next.js, create a /auth/callback route handler that calls exchangeCodeForSession. For SPAs, set up an onAuthStateChange listener that detects the SIGNED_IN event.
Best practices
- Configure custom SMTP before going to production to avoid the 2 emails/hour default limit
- Add all your deployment URLs (production, preview, local) to the redirect URL allowlist
- Show clear feedback after sending the magic link so users know to check their email
- Set shouldCreateUser: false if you want to prevent new accounts from being created via magic link
- Use onAuthStateChange to detect when the user returns from the magic link callback
- Disable the form submit button while the magic link is being sent to prevent duplicate emails
- Set the sender email to an address on your own domain for better deliverability
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to add passwordless magic link login to my React app using Supabase. Show me how to enable the email provider, call signInWithOtp, handle the redirect callback, and build a simple login form with loading and success states.
Create a React component that implements magic link login with Supabase. It should have an email input form, call signInWithOtp with emailRedirectTo, show a success message after sending, and use onAuthStateChange to detect when the user is signed in after clicking the magic link.
Frequently asked questions
Does magic link login create a new account if the user does not exist?
Yes, by default signInWithOtp creates a new user if the email is not registered. Set shouldCreateUser: false in the options to prevent this and only allow existing users to receive magic links.
How long does a magic link stay valid?
The default magic link expiration is 24 hours. You can configure this in Dashboard > Authentication > Auth Settings by changing the OTP expiry time.
Can I use magic links alongside password login?
Yes. Both methods can coexist. Users can sign up with email/password and later use magic links, or vice versa. The same user account works with both methods.
Why are my magic link emails going to spam?
The default Supabase SMTP sender may be flagged by spam filters. Configure a custom SMTP provider with your own domain and set up SPF, DKIM, and DMARC records for your sending domain to improve deliverability.
Can I customize the magic link email content?
Yes. Go to Dashboard > Authentication > Email Templates and edit the Magic Link template. Use {{ .ConfirmationURL }} for the link and customize the HTML to match your branding.
What is the difference between magic link and OTP code?
Both use signInWithOtp. Magic links send a clickable URL via email. Phone OTP sends a numeric code via SMS. For email, Supabase sends a magic link by default. You can also enable email OTP codes in the Dashboard if you prefer a code-based flow.
Can RapidDev help implement magic link authentication for my app?
Yes. RapidDev can set up magic link login with custom SMTP, branded email templates, proper callback handling for your framework, and additional features like invite-only access or domain restrictions.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation