Supabase Magic Link: Passwordless Authentication Guide

Magic link authentication provides a modern, passwordless login experience where users receive a one-time secure link via email instead of managing passwords, eliminating password-related security risks, reducing support requests for forgotten credentials, and improving conversion rates by simplifying the signup process. This comprehensive guide covers implementing magic link authentication with Supabase, building React components for email collection and verification, handling authentication callbacks, managing user sessions after magic link login, customizing email templates, implementing rate limiting, and combining magic links with traditional authentication methods. Magic links are increasingly popular for consumer applications, internal tools, and SaaS products where user convenience and security are paramount. Unlike passwords that can be weak, reused, or phished, magic links provide time-limited, single-use authentication tokens sent directly to verified email addresses. Before proceeding, understand authentication fundamentals and email authentication basics.
Why Magic Links?
| Benefit | Description | User Impact | Security Impact |
|---|---|---|---|
| No Passwords | Eliminates password creation/storage | Faster signup | No password breaches |
| Better UX | One-click login from email | Fewer steps | Reduces phishing risk |
| Lower Support | No forgotten password requests | Less friction | N/A |
| Higher Conversion | Simpler signup process | More signups | N/A |
| Email Verification | Automatic email validation | Seamless | Verifies email ownership |
How Magic Links Work
- User enters email address in your application
- Supabase sends secure, time-limited link to the email
- User clicks link in their email inbox
- User is redirected to your app and automatically logged in
- Session is created and stored in browser
- User can access protected resources
Basic Implementation
// MagicLinkLogin.jsx
import { useState } from 'react'
import { supabase } from './supabaseClient'
function MagicLinkLogin() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [sent, setSent] = useState(false)
const [error, setError] = useState(null)
async function handleMagicLink(e) {
e.preventDefault()
setLoading(true)
setError(null)
const { data, error } = await supabase.auth.signInWithOtp({
email: email,
options: {
// URL to redirect after clicking magic link
emailRedirectTo: `${window.location.origin}/dashboard`
}
})
setLoading(false)
if (error) {
setError(error.message)
return
}
setSent(true)
}
if (sent) {
return (
<div className="magic-link-sent">
<h2>Check Your Email</h2>
<p>We sent a magic link to <strong>{email}</strong></p>
<p>Click the link in the email to log in.</p>
<button onClick={() => setSent(false)}>Send Another Link</button>
</div>
)
}
return (
<div className="magic-link-form">
<h2>Log In with Magic Link</h2>
<p>Enter your email to receive a login link</p>
<form onSubmit={handleMagicLink}>
<input
type="email"
placeholder="[email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
</form>
</div>
)
}
export default MagicLinkLoginHandling Authentication Callback
// App.jsx - Handle magic link callback
import { useEffect } from 'react'
import { supabase } from './supabaseClient'
import { useNavigate } from 'react-router-dom'
function App() {
const navigate = useNavigate()
useEffect(() => {
// Listen for auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session.user.email)
// Redirect to dashboard after successful login
navigate('/dashboard')
}
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed')
}
}
)
return () => subscription.unsubscribe()
}, [])
return (
// Your app routes
)
}
// Alternative: Check for existing session on mount
function Dashboard() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
checkUser()
}, [])
async function checkUser() {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
setLoading(false)
}
if (loading) return <div>Loading...</div>
if (!user) return <div>Please log in</div>
return (
<div>
<h1>Welcome {user.email}</h1>
{/* Dashboard content */}
</div>
)
}Combining with Password Auth
// LoginPage.jsx - Offer both options
import { useState } from 'react'
import { supabase } from './supabaseClient'
function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [useMagicLink, setUseMagicLink] = useState(false)
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState(null)
async function handleSubmit(e) {
e.preventDefault()
setLoading(true)
setMessage(null)
if (useMagicLink) {
// Magic link login
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) {
setMessage({ type: 'error', text: error.message })
} else {
setMessage({
type: 'success',
text: 'Check your email for the login link!'
})
}
} else {
// Password login
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
setMessage({ type: 'error', text: error.message })
}
}
setLoading(false)
}
return (
<div>
<h2>Log In</h2>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
{!useMagicLink && (
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
)}
{message && <p className={message.type}>{message.text}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Please wait...' :
useMagicLink ? 'Send Magic Link' : 'Log In'}
</button>
</form>
<button
type="button"
onClick={() => setUseMagicLink(!useMagicLink)}
className="toggle-method"
>
{useMagicLink ?
'Use password instead' :
'Use magic link instead'
}
</button>
</div>
)
}
export default LoginPageCustomizing Email Templates
Customize magic link emails in your Supabase Dashboard under Authentication > Email Templates. Modify the subject line, email body, and branding to match your application. Use variables like {{ .ConfirmationURL }} for the magic link and {{ .SiteURL }} for your app URL. Add your logo, brand colors, and custom messaging to improve trust and click-through rates.
Security Considerations
- Link Expiration: Magic links expire after a set time (default 1 hour) preventing old links from working
- Single Use: Each magic link can only be used once for security
- Rate Limiting: Supabase limits magic link requests to prevent spam and abuse
- HTTPS Required: Always use HTTPS to prevent link interception
- Email Security: Magic links are only as secure as the user's email account
- Add RLS Policies: Always implement Row Level Security to protect user data
Best Practices
- Clear Instructions: Tell users to check their email and spam folder
- Show Success Message: Confirm the email was sent with the exact email address
- Allow Resending: Let users request another link if needed
- Provide Alternative: Offer password login as backup option
- Customize Emails: Brand your magic link emails for trust and recognition
- Handle Errors Gracefully: Show helpful messages for invalid emails or rate limits
Common Issues
- Email Not Received: Check spam folder, verify email provider settings, confirm SMTP configuration in Supabase
- Link Expired: Request a new magic link; default expiration is 1 hour
- Redirect Not Working: Verify emailRedirectTo URL matches your site URL in Supabase settings
- Rate Limit Errors: Wait before requesting another link; default is 4 emails per hour per email address
Next Steps
- Add OAuth: Implement social login with Google and GitHub for more options
- Secure Data: Implement Row Level Security policies to protect user information
- Build Profiles: Create user profile pages with file uploads for avatars
- Production Deployment: Integrate with Next.js for server-side rendering
Conclusion
Magic link authentication offers a modern, passwordless login experience that improves user experience while maintaining strong security through time-limited, single-use tokens sent to verified email addresses. By eliminating passwords, you reduce support burden, improve conversion rates, and provide a seamless authentication flow that users appreciate. Supabase makes magic link implementation straightforward with built-in support, customizable email templates, and automatic session management. Whether used exclusively or combined with traditional password authentication, magic links provide flexibility in how users access your application. Always remember to customize email templates for branding, implement proper error handling, and protect user data with Row Level Security policies. With magic link authentication mastered, continue building secure applications with OAuth integration and comprehensive security policies.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


