Magic links have become the gold standard for passwordless authentication. No password to remember, no credentials to leak, just a simple email click that logs users in. For self-hosted Supabase deployments, setting up magic links requires proper SMTP configuration and attention to a few gotchas that trip up many developers.
This guide walks through everything you need to get magic links working reliably in your self-hosted environment.
Why Magic Links for Self-Hosted Supabase
Passwordless authentication eliminates password-related security risks: weak passwords, credential stuffing, and password reuse across services. For self-hosted deployments, magic links offer additional benefits:
Reduced attack surface: No password hashes to protect or migrate. Your user table becomes simpler and safer.
Better user experience: Users click a link instead of remembering (or resetting) passwords. Conversion rates improve when signup friction decreases.
Simpler auth flow: No password reset flows to build and maintain. Magic links handle both signup and signin with the same mechanism.
The trade-off? You need reliable email delivery. For self-hosted Supabase, that means configuring your own SMTP provider.
Prerequisites
Before configuring magic links, ensure you have:
- A working self-hosted Supabase deployment
- An SMTP provider (SendGrid, Mailgun, AWS SES, Resend, or your own mail server)
- DNS access for SPF/DKIM records to improve deliverability
- A configured Site URL in your Supabase auth settings
If you're still using Supabase's built-in email service, note that it's limited to 3 emails per hour—fine for local development, but unusable for production.
Configuring SMTP for Magic Links
Magic links depend entirely on email delivery. Without proper SMTP configuration, your users will never receive their login links.
Environment Variables
Add these variables to your .env file or environment configuration:
# SMTP Configuration SMTP_HOST=smtp.your-provider.com SMTP_PORT=587 SMTP_USER=your-smtp-username SMTP_PASS=your-smtp-password SMTP_SENDER_NAME=Your App Name [email protected] # Important: Don't leave SMTP_PORT empty # An empty port value causes supabase-auth service to fail
A common mistake is leaving SMTP_PORT commented out or empty. The auth service won't start properly without a valid port value, even if other SMTP parameters are set.
Docker Compose Configuration
In your docker-compose.yml, ensure the auth service has access to SMTP variables:
services:
auth:
environment:
GOTRUE_SMTP_HOST: ${SMTP_HOST}
GOTRUE_SMTP_PORT: ${SMTP_PORT}
GOTRUE_SMTP_USER: ${SMTP_USER}
GOTRUE_SMTP_PASS: ${SMTP_PASS}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
GOTRUE_MAILER_AUTOCONFIRM: false
Testing SMTP Configuration
Before implementing magic links in your app, verify email delivery works:
# Check auth service logs for SMTP errors
docker logs supabase-auth 2>&1 | grep -i smtp
# Send a test signup to verify emails arrive
curl -X POST 'http://localhost:8000/auth/v1/signup' \
-H "Content-Type: application/json" \
-H "apikey: YOUR_ANON_KEY" \
-d '{"email": "[email protected]", "password": "temporary123"}'
If emails aren't arriving, check spam folders first, then verify your SPF and DKIM records are properly configured with your email provider.
Enabling Magic Link Authentication
With SMTP configured, magic links are enabled by default in Supabase Auth. The configuration happens in your auth settings:
# In your supabase config.toml or environment GOTRUE_EXTERNAL_EMAIL_ENABLED: true GOTRUE_MAILER_AUTOCONFIRM: false GOTRUE_MAILER_OTP_EXP: 3600 # Link expires in 1 hour GOTRUE_RATE_LIMIT_EMAIL_SENT: 60 # One email per 60 seconds per user
Site URL Configuration
The Site URL determines where users land after clicking the magic link. This must be explicitly configured:
GOTRUE_SITE_URL: https://yourdomain.com GOTRUE_URI_ALLOW_LIST: https://yourdomain.com/*,https://staging.yourdomain.com/*
Any URL not in this allow list will be rejected, protecting against open redirect attacks.
Implementing Magic Links in Your Application
Client-Side Implementation
Use the Supabase client library to trigger magic link emails:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-supabase-url.com',
'your-anon-key'
)
// Send magic link
async function sendMagicLink(email: string) {
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourdomain.com/auth/callback',
// Set to false to prevent auto-signup for unknown emails
shouldCreateUser: true,
}
})
if (error) {
console.error('Failed to send magic link:', error.message)
return false
}
return true
}
Handling the Callback
When users click the magic link, Supabase redirects them to your callback URL with tokens in the URL fragment:
// pages/auth/callback.ts (Next.js example)
import { createClient } from '@supabase/supabase-js'
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function AuthCallback() {
const router = useRouter()
useEffect(() => {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Exchange the token from URL
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN' && session) {
router.push('/dashboard')
}
})
}, [])
return <div>Completing sign in...</div>
}
Customizing Magic Link Emails
Default magic link emails are functional but generic. Customize them to match your brand.
Email Template Configuration
Create custom templates in your Supabase configuration:
<!-- templates/magic_link.html -->
<!DOCTYPE html>
<html>
<head>
<style>
.button {
background-color: #10b981;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
display: inline-block;
}
</style>
</head>
<body>
<h2>Sign in to Your App</h2>
<p>Click the button below to sign in. This link expires in 1 hour.</p>
<a href="{{ .ConfirmationURL }}" class="button">Sign In</a>
<p>If you didn't request this email, you can safely ignore it.</p>
</body>
</html>
The key variable is {{ .ConfirmationURL }}—this generates a magic link. If you use {{ .Token }} instead, users receive a 6-digit OTP code to enter manually.
OTP vs Magic Link
Both use the same underlying mechanism. The difference is in template content:
- Magic Link: Include
{{ .ConfirmationURL }}for a clickable link - OTP Code: Include
{{ .Token }}for a code users type into your app
You can offer both by building a flow where the magic link lands on a page that also accepts manual OTP entry.
Troubleshooting Common Issues
Links Redirect to Wrong URL
A frequent issue on self-hosted deployments: magic links redirect to http://kong/auth/v1/verify?token=... instead of your actual domain.
Fix: Ensure your GOTRUE_SITE_URL and API_EXTERNAL_URL environment variables are set to your actual domain, not internal Docker service names:
API_EXTERNAL_URL: https://yourdomain.com GOTRUE_SITE_URL: https://yourdomain.com
"Token has expired or is invalid" Error
This commonly happens when corporate email security (like Microsoft's Safe Links) pre-fetches URLs before users see them. The link gets consumed by the security scanner.
Solutions:
Increase OTP expiration (while keeping it reasonable):
GOTRUE_MAILER_OTP_EXP: 3600 # 1 hour
Switch to OTP codes instead of magic links for corporate users—codes aren't consumed by URL pre-fetching
Implement two-step verification: The magic link opens a page where users must click "Confirm" to actually authenticate
Emails Not Arriving
Check in order:
- SMTP logs:
docker logs supabase-auth 2>&1 | grep -i mail - Rate limits: Users can only request one link per 60 seconds by default
- Spam folders: Magic links often trigger spam filters
- SPF/DKIM: Verify your DNS records match your SMTP provider's requirements
- SMTP credentials: Test with a simple mail client to rule out Supabase issues
Session Management Issues
Magic links create sessions just like password authentication. If users report being logged out unexpectedly, check your token configuration:
GOTRUE_JWT_EXP: 3600 # Access token lifetime GOTRUE_JWT_AUD: authenticated
Access tokens default to 1 hour. The client library automatically refreshes sessions before expiration, but issues arise with clock skew or very short expiration times.
Security Considerations
Rate Limiting
Protect against enumeration attacks by limiting magic link requests:
GOTRUE_RATE_LIMIT_EMAIL_SENT: 60 # Seconds between emails per user
Consider additional rate limiting at your reverse proxy level to prevent abuse.
Redirect URL Validation
Only allow redirects to URLs you control:
GOTRUE_URI_ALLOW_LIST: https://yourdomain.com/*,https://app.yourdomain.com/*
Never use wildcards that could match attacker-controlled domains.
Link Expiration
Balance security with usability. One hour expiration (3600 seconds) works for most use cases. Longer expiration increases risk if links are forwarded or stored insecurely.
GOTRUE_MAILER_OTP_EXP: 3600 # Maximum recommended: 86400 (24 hours)
Supabase enforces a hard limit of 86400 seconds to prevent brute force attacks.
Magic Links with Supascale
If you're managing self-hosted Supabase with Supascale, SMTP configuration is handled through the dashboard. You can:
- Configure SMTP credentials through the UI instead of environment files
- Set up custom domains for professional-looking magic link URLs
- Manage multiple projects with different auth configurations
- Back up your auth configuration alongside your database backups
The one-time $39.99 license covers unlimited projects, making it practical to run separate staging and production environments with different auth settings.
Conclusion
Magic links eliminate password management headaches while providing a secure, user-friendly authentication experience. For self-hosted Supabase, the main requirement is reliable SMTP delivery.
Key takeaways:
- Configure SMTP properly—empty or missing values cause silent failures
- Set your Site URL explicitly—internal Docker URLs cause redirect issues
- Customize templates for better brand consistency and user trust
- Handle edge cases like email prefetching with OTP alternatives
- Rate limit aggressively to prevent enumeration attacks
With these configurations in place, magic links work as reliably on self-hosted Supabase as they do on the managed cloud platform.
