Add Magic Link Login to Your SaaS (and When to Skip It)
A founder-friendly magic link authentication tutorial with Supabase and Next.js: the signInWithOtp call, the callback route, the deliverability trap, and when passwordless is the wrong call.
· Justin Boggs

Photo by Davide Baraldi on Unsplash
To add magic link login to a Next.js SaaS with Supabase, you do three things: enable the email provider in the Supabase dashboard, call supabase.auth.signInWithOtp({ email, options: { emailRedirectTo } }) from a form when someone enters their address, and create a callback route that exchanges the link's code for a session. The user types their email, gets a one-time link in their inbox, clicks it, and they are signed in — no password anywhere. It is genuinely the fastest auth to wire up. The part nobody warns you about is that your login now depends entirely on email deliverability, and that is where magic links quietly cost you signups.
TL;DR
- Magic link login sends a one-time sign-in link to the user's email instead of asking for a password. In Supabase it is the
signInWithOtpcall plus a callback route.- Setup takes about fifteen minutes: enable the email provider, add a form, handle the redirect, configure your redirect URLs.
- The deliverability trap is the real cost. If the link lands in spam or takes four minutes to arrive, your login is broken and you will never see the error.
- Configure a real SMTP sender (Resend, Postmark) before launch — the built-in Supabase email service is rate-limited and not for production.
- Skip magic links, or offer a password fallback, when your users switch devices mid-login, live in shared inboxes, or need to sign in dozens of times a day.
What a magic link actually is
A magic link is a one-time, time-limited URL emailed to a user that signs them in when clicked, replacing the password entirely. The user proves they own the email address, and owning the address is treated as proof of identity. That is the whole mechanism.
Under the hood it is the same one-time-password flow that powers "email me a code" logins — the link just carries the code so the user does not have to type it. Supabase calls both of these signInWithOtp. When someone clicks the link, they land back on your app with a token in the URL, and your app trades that token for a real session.
The appeal for a non-technical founder is obvious. You do not build a password field, a "forgot password" flow, a reset-email template, or a password-strength meter. You do not store password hashes, which means a database leak cannot expose credentials that people reuse across other sites. As the Ping Identity team puts it, magic links eliminate credential stuffing entirely because there is no stored password to stuff.
The tradeoff is that you have moved the entire security boundary to the user's email account. If someone can read their inbox, they can sign in as them. That is fine for most SaaS — email is already the account-recovery path for password logins anyway — but it is a real consideration I will come back to. First, the wiring.
Step 1: Enable the email provider in Supabase
Magic links ride on Supabase's email provider, so that has to be on. In the Supabase dashboard, go to Authentication, then Sign In / Providers, and confirm the Email provider is enabled. That is the same provider that handles email-and-password signups, so it is usually already on.
Two settings matter here. The first is the Email OTP expiration, which controls how long a magic link stays valid. The default is one hour. Per Supabase's passwordless login docs, an expiry longer than 86,400 seconds — 24 hours — is strongly discouraged, and Supabase will only let you set it that high through the Management API. Keep it short. A link that works for a week is a link an attacker has a week to use.
The second is the rate limit. By default a user can only request a new magic link once every 60 seconds. That protects you from someone hammering the "send me a link" button and racking up email costs, but it also means your UI needs to tell the user to wait, not silently fail.
If you are still choosing an auth approach at all, the broader tradeoffs between Supabase Auth, Clerk, and NextAuth are covered in authentication choices for SaaS in 2026. This tutorial assumes you have landed on Supabase, which handles passwordless email about as cleanly as anything on the market.
One thing to decide now: do you want magic links to also create accounts, or only sign in existing users? The signInWithOtp call has a shouldCreateUser option. Leave it true and a first-time visitor who enters their email gets an account automatically. Set it false and unknown emails are rejected — useful if signup happens through a different flow.
Step 2: Wire up signInWithOtp
The client-side code is short. From a form where the user has entered their email, you call signInWithOtp with that address and a emailRedirectTo URL pointing at your callback route:
'use client'
import { useState } from 'react'
import { createClient } from '@/utils/supabase/client'
export function MagicLinkForm() {
const supabase = createClient()
const [email, setEmail] = useState('')
const [sent, setSent] = useState(false)
async function sendLink(e: React.FormEvent) {
e.preventDefault()
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
shouldCreateUser: true,
},
})
if (!error) setSent(true)
}
if (sent) {
return <p>Check your inbox for a sign-in link.</p>
}
return (
<form onSubmit={sendLink}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<button type="submit">Email me a link</button>
</form>
)
}
Notice what happens after you send the link: you show a "check your inbox" state and stop. There is no password to validate, no error to catch beyond "did the send succeed." That simplicity is the entire pitch.
The emailRedirectTo value has to be on Supabase's allowlist or the redirect silently fails. In the dashboard, under Authentication, then URL Configuration, add your Site URL and every Redirect URL you use — http://localhost:3000/auth/callback for local development and https://yourapp.com/auth/callback for production. This is the single most common reason a magic link "does nothing" when clicked: the destination is not on the list, so Supabase refuses to send the user there.
Step 3: Handle the callback
When the user clicks the link, they arrive at /auth/callback with a code in the query string. Your job is to exchange that code for a session and then send them into the app. The route handler looks like this:
// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/dashboard'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/auth/error`)
}
This is the PKCE flow Supabase recommends for server-rendered Next.js apps, and it is the same callback route you would use for OAuth login with Google or GitHub — magic links and social login share this exact endpoint. If you already built OAuth, you may not need to write this route again; you just need it to exist.
The next parameter lets you send people somewhere specific after login — a paywalled page they were trying to reach, an onboarding step, wherever. Default it to your dashboard and you are done. Once exchangeCodeForSession succeeds, Supabase sets the session cookie and every server component in your app can read the logged-in user.
If you have Claude Code or Cursor open, the prompt that gets you this whole flow in one shot is roughly: "Add magic link login with Supabase to this Next.js app. Enable it with signInWithOtp, add a callback route at app/auth/callback/route.ts that runs exchangeCodeForSession, and add the redirect URLs I need to configure." Reading the diff it produces is a skill worth building — reviewing AI-generated code you can't write yourself walks through exactly what to check.
The deliverability trap nobody warns you about
Here is the part that turns a fifteen-minute integration into a support headache. With password login, if the login breaks, the user sees an error and tells you. With magic links, if the email never arrives, nothing happens — no error, no signal, just a person staring at "check your inbox" with an empty inbox. You will never see that failure in your logs. The user just leaves.
Two things cause it. First, the built-in Supabase email service is meant for development only. It is heavily rate-limited and sends from a shared address that spam filters distrust. Ship on it and a chunk of your links land in spam or get throttled. Before launch, connect a real SMTP provider — Resend and Postmark both plug into Supabase's SMTP settings and dramatically improve inbox placement. The Resend vs Postmark vs Mailgun comparison covers which to pick.
Second, even with good SMTP, email is slow and imperfect. As one passwordless UX and security checklist notes, a link that takes a few minutes to arrive, lands in spam, or requires switching to a phone to read the email adds friction at the worst possible moment — the login. Every extra step is a chance to lose the person.
The defenses are practical. Send the email from a subdomain with proper SPF, DKIM, and DMARC records so filters trust it. Keep the email plain and transactional — a long HTML template with images looks like marketing and gets filtered. Set up a status page so you know when sends are failing; adding a free status page with BetterStack takes twenty minutes. And always show the user a clear "didn't get it? resend in 60 seconds" affordance, because sometimes the answer really is just "try again."
When passwordless is the wrong call
Magic links are not universally better. There are specific situations where they actively hurt, and an honest founder should know them before committing.
Device-switching workflows. If your users routinely sign up on a laptop but read email on a phone, every login means picking up a second device, finding the email, and clicking a link that may open in the wrong browser. That context switch kills conversion. Power users who sign in constantly feel the same drag — a password in a manager is one autofill; a magic link is a round trip through the inbox every time.
Shared or role inboxes. Teams that log in through support@ or billing@ share one inbox, and a magic link sent there is a sign-in anyone on that alias can use. That is either a feature or a security hole depending on your product, but it is rarely what you intended.
Email as a single point of failure. Because the security boundary is the inbox, a compromised email account means a compromised app account, with no second factor in the way. For anything touching money or sensitive data, magic-link-only is thin. Pair it with a second factor, or offer password-plus-2FA as an option.
The pattern I actually recommend for most SaaS: offer magic links and a password option, and let people choose. New users overwhelmingly take the magic link because it is one less thing to invent. Power users set a password. You cover both without betting your entire login on email deliverability. Where this fits in the bigger picture of trial and onboarding friction is covered in customer onboarding flows that don't bore users to death.
Frequently asked questions
Are magic links less secure than passwords?
Not inherently — they trade one risk for another. Magic links remove password reuse and credential-stuffing risk entirely, but they make your app exactly as secure as the user's email account. For most SaaS that is a net win, since email already controls password resets. For anything high-stakes, add a second factor rather than relying on the inbox alone.
How long should a magic link stay valid?
Short. Supabase defaults to one hour, which is reasonable. The docs strongly discourage anything over 24 hours and gate it behind the Management API. A shorter window means a smaller opportunity for an intercepted or forwarded link to be abused, and one hour is plenty of time for a normal person to check their email.
Why isn't my magic link email arriving?
Two usual causes. If you are still on Supabase's built-in email service, it is rate-limited and prone to spam filtering — switch to a real SMTP provider like Resend before launch. If you already have SMTP, check that your sending domain has SPF, DKIM, and DMARC configured, and look in the spam folder. The 60-second rate limit can also block rapid repeat requests.
Can I use magic links and passwords together?
Yes, and for most products you should. Supabase supports email-and-password, magic links, and OAuth on the same user, so you can offer all three and let people pick. Magic links win first signups; passwords serve power users; OAuth covers the "sign in with Google" crowd. They all resolve to the same Supabase user object.
Do magic links work for mobile apps?
They can, but deep-linking the callback back into the app is fiddly, and the device-switching problem is worse on mobile. Many mobile-first products use OTP codes instead — the same signInWithOtp flow, but the user types a six-digit code rather than clicking a link, which avoids the "which app opens the link" mess entirely.
The honest recommendation
Magic links are the fastest auth you can ship and a genuinely good default for a new SaaS — right up until email deliverability turns a silent failure into lost signups you never see. Wire up signInWithOtp, add the callback route, configure your redirect URLs, and you have working passwordless login in an afternoon. Then spend the next hour on the part that actually matters: a real SMTP sender, proper DNS records, and a password fallback for the users who need it. That combination gets you the low-friction signup without betting your whole login on the inbox.
If you are building a SaaS with AI coding tools and want auth, billing, and email already wired together, Coding Capybaras is the free boilerplate I built for exactly this workflow — the Supabase auth flow above ships in it out of the box.