Magic Links for Self-Hosted Supabase: Complete Setup Guide

Set up passwordless magic link authentication for self-hosted Supabase with SMTP configuration, email templates, and troubleshooting.

Cover Image for Magic Links for Self-Hosted Supabase: Complete Setup Guide

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.

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:

  1. A working self-hosted Supabase deployment
  2. An SMTP provider (SendGrid, Mailgun, AWS SES, Resend, or your own mail server)
  3. DNS access for SPF/DKIM records to improve deliverability
  4. 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.

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.

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.

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>
}

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.

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

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:

  1. Increase OTP expiration (while keeping it reasonable):

    GOTRUE_MAILER_OTP_EXP: 3600  # 1 hour
    
  2. Switch to OTP codes instead of magic links for corporate users—codes aren't consumed by URL pre-fetching

  3. Implement two-step verification: The magic link opens a page where users must click "Confirm" to actually authenticate

Emails Not Arriving

Check in order:

  1. SMTP logs: docker logs supabase-auth 2>&1 | grep -i mail
  2. Rate limits: Users can only request one link per 60 seconds by default
  3. Spam folders: Magic links often trigger spam filters
  4. SPF/DKIM: Verify your DNS records match your SMTP provider's requirements
  5. 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.

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.

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:

  1. Configure SMTP properly—empty or missing values cause silent failures
  2. Set your Site URL explicitly—internal Docker URLs cause redirect issues
  3. Customize templates for better brand consistency and user trust
  4. Handle edge cases like email prefetching with OTP alternatives
  5. 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.


Further Reading