Supabase

Supabase "Error sending confirmation email" (500): 5 fixes

Email confirmations not sending from Supabase? Learn the exact causes and fixes for SMTP, template, and configuration issues in 10 minutes.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
10 min read

"Error sending confirmation email": what Supabase is telling you#

Sign-up calls fail with HTTP 500 and this message in the client (older GoTrue builds say "mail", as in supabase/supabase issue 20739):

text
AuthApiError: Error sending confirmation mail
status: 500

The fix, in order of likelihood: Supabase's built-in SMTP is capped at 2 messages per hour and only reaches addresses belonging to your project's team — Supabase's own documentation states it is "not meant for production use" — the moment you test with a real user address, Supabase Auth refuses to send and returns the error code email_address_not_authorized (HTTP 400, listed in the Auth error codes). Configure a custom SMTP provider (Resend, Postmark, SES) in Dashboard → Authentication → Emails → SMTP Settings first; that alone fixes it for most people. If the email still doesn't arrive, the 4 next-most-common causes — broken template links, missing SPF/DKIM records, an already-confirmed user, and email rate limits — are covered below in the order to check them.

This article is part of our comprehensive Supabase Authentication & Authorization Patterns guide.

Why Supabase Email Confirmations Fail#

There are 5 main reasons why confirmation emails don't send:

  1. Email Rate Limits - the built-in SMTP sends at most 2 emails per hour per project (not a free-tier quirk), and custom SMTP starts at 30 per hour
  2. SMTP Not Configured - Default email service is unreliable
  3. Email Template Issues - Broken confirmation links or template errors
  4. Domain/DNS Problems - SPF/DKIM records not configured
  5. User Already Confirmed - Email was already verified

Quick Diagnostic Checklist#

Before diving into fixes, run through this checklist:

bash
## Check Supabase logs
## Go to: Dashboard > Logs > Auth Logs
## Look for: "user_confirmation_requested" or error codes such as
## email_address_not_authorized / over_email_send_rate_limit
 
## Check user status in database
SELECT email, email_confirmed_at, confirmation_token 
FROM auth.users 
WHERE email = 'user@example.com';
 
## Verify SMTP settings
## Go to: Dashboard > Authentication > Emails > SMTP Settings
## Check: "Enable custom SMTP" is switched on

user_confirmation_requested is the audit action Supabase Auth records when it sends a sign-up confirmation (there is no email_confirmation_sent event in its audit action list); the two error codes are explained in the Auth error codes reference. SMTP lives under Authentication → Emails → SMTP Settings in the current dashboard.

The default Supabase email service is unreliable. Use a custom SMTP provider:

Step 1: Choose an SMTP Provider#

Best options for production:

Step 2: Configure SMTP in Supabase#

typescript
// 1. Go to Supabase Dashboard > Authentication > Emails
// 2. Open the "SMTP Settings" tab
// 3. Switch on "Enable custom SMTP"
 
// Example SendGrid Configuration:
Host: smtp.sendgrid.net
Port: 587
Username: apikey
Password: YOUR_SENDGRID_API_KEY
Sender email: noreply@iloveblogs.blog
Sender name: Your App Name

Once saved, Supabase Auth sends to every address, but it imposes a low limit of 30 messages per hour on the new SMTP setup until you raise it under Authentication → Rate Limits, according to the custom SMTP guide.

Alternative setup reference: SMTP Senpai#

For a provider-specific walkthrough, MailSenpai's Supabase SMTP setup guide shows the sender-domain verification and dashboard fields. Its documented host is relay.mailsenpai.com, using port 587 or 2525 with STARTTLS and an address on your verified sending domain. Use the SMTP credentials and DNS records supplied in your own provider account; keep the credentials out of client-side code.

According to MailSenpai's published free-plan terms, checked on 30 October 2026, the allowance is 5,000 emails per month, 200 per day and 30 accepted recipients in the preceding 60 minutes, with one sending domain and seven-day logs. Supabase's separate Auth rate limit still applies. MailSenpai does not queue rate-limited free-plan submissions for later delivery, so distinguish a rate-limit rejection from a message accepted for sending.

MailSenpai supplied a configuration/reproduction example for this reference. It has not been verified here with a live Supabase sign-up and does not establish inbox delivery or endorsement by Supabase. Check the Auth result, provider event, received message and confirmation callback separately.

Step 3: Verify SMTP Connection#

typescript
// The dashboard has no "send test email" button for SMTP.
// Test with a real flow instead:
// Authentication > Users > Add user > Send invitation
// (use an address that is NOT a member of your Supabase team)
// Check: Email arrives in inbox (not spam), and no
// email_address_not_authorized error is returned

Invitations from the Users page use the project's configured SMTP settings (custom SMTP guide). A delivered invitation to a non-team address confirms that the invitation flow used custom SMTP for that test. It does not verify the sign-up confirmation template, the application's redirect/callback handling, or inbox placement for other recipients. Test a new sign-up and its confirmation callback separately with a mailbox you control.

Step 4: Update Environment Variables#

bash
## .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
 
## Only if your own app code sends mail (e.g. Nodemailer).
## Supabase Auth never reads these: its SMTP settings live in the dashboard.
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-api-key

On hosted Supabase, the Auth server takes its SMTP credentials from the dashboard or the Management API (smtp_host, smtp_user, smtp_pass), as the custom SMTP guide shows, never from your Next.js environment.

Solution 2: Fix Email Template Configuration#

Check Confirmation URL#

The confirmation link must point to your application:

typescript
// Supabase Dashboard > Authentication > URL Configuration
// Site URL: https://www.iloveblogs.blog
// Redirect URLs: https://www.iloveblogs.blog/auth/callback
 
// With @supabase/ssr (PKCE flow) and the route below, the template
// must send the token hash to your own route:
//   {{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email
// NOT {{ .ConfirmationURL }}: it verifies on Supabase, then redirects
//   with ?code=..., which this route never reads
// NOT {{ .Token }}: that is the 6-digit OTP, not a link token

@supabase/ssr clients use the PKCE flow by default, and Supabase's PKCE sign-up guide has the confirmation template send token_hash to a route that calls verifyOtp. The default {{ .ConfirmationURL }} link instead sends the user back to your redirect URL with a ?code= parameter (PKCE flow), so a route that only reads token_hash sends every confirmed user to its error page.

Update Email Template#

html
<!-- Go to: Dashboard > Authentication > Emails > Templates > Confirm sign up -->
<h2>Confirm your signup</h2>
<p>Follow this link to confirm your account:</p>
<p><a href="{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email">Confirm your email</a></p>
<p>Or copy and paste this URL into your browser:</p>
<p>{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email</p>

Handle Confirmation in Next.js#

@supabase/auth-helpers-nextjs is deprecated — do not start new code with it. Use @supabase/ssr instead (createServerClient for Route Handlers, Server Components, and Server Actions; createBrowserClient for Client Components). In Next.js 15+ cookies() is async, so you await it before building the client.

typescript
// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
 
export async function GET(request: Request) {
  const requestUrl = new URL(request.url)
  const token_hash = requestUrl.searchParams.get('token_hash')
  const type = requestUrl.searchParams.get('type')
  const next = requestUrl.searchParams.get('next') ?? '/'
 
  if (token_hash && type) {
    const cookieStore = await cookies()
    const supabase = createServerClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
      {
        cookies: {
          getAll() {
            return cookieStore.getAll()
          },
          setAll(cookiesToSet) {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          },
        },
      }
    )
 
    const { error } = await supabase.auth.verifyOtp({
      type: type as any,
      token_hash,
    })
 
    if (!error) {
      return NextResponse.redirect(new URL(next, request.url))
    }
  }
 
  // Return error page
  return NextResponse.redirect(new URL('/auth/error', request.url))
}

Solution 3: Configure DNS Records (Production)#

For production, configure SPF and DKIM records:

SPF Record#

dns
## Add TXT record to your domain:
Type: TXT
Name: @
Value: v=spf1 include:sendgrid.net ~all
 
## For SendGrid, check their documentation for exact value

DKIM Record#

dns
## SendGrid provides DKIM records in their dashboard
## Add 3 CNAME records they provide
## Example:
Type: CNAME
Name: s1._domainkey
Value: s1.domainkey.u12345.wl.sendgrid.net

Verify DNS Configuration#

bash
## Check SPF record
nslookup -type=TXT iloveblogs.blog
 
## Check DKIM record
nslookup -type=CNAME s1._domainkey.iloveblogs.blog
 
## Test email deliverability
## Use: mail-tester.com

Solution 4: Handle Rate Limits#

Supabase rate-limits auth emails on every project, not just the free tier. The rate limits page sets the built-in provider at 2 emails per hour per project and the sign-up confirmation window at 60 seconds per user. Custom SMTP starts at 30 per hour. Both limits return HTTP 429 with the error code over_email_send_rate_limit (Auth error codes). The message is either "email rate limit exceeded" or "For security purposes, you can only request this after N seconds." (see mail.go and errors.go in the Auth server source). The second message does not contain "rate limit", so matching on the message text misses it. A retry a few seconds later hits the same limit, so detect the code and tell the user to wait:

typescript
// Detect the email rate limit instead of retrying (client component)
import { createBrowserClient } from '@supabase/ssr'
 
async function sendConfirmationEmail(email: string) {
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
 
  try {
    const { error } = await supabase.auth.signUp({
      email,
      password: 'user-password',
      options: {
        emailRedirectTo: `${window.location.origin}/auth/callback`,
      },
    })
 
    if (error) {
      // 429 over_email_send_rate_limit: the project's hourly email cap or
      // the 60-second per-user window. Retrying in a loop cannot beat either.
      if (error.code === 'over_email_send_rate_limit') {
        return {
          success: false,
          error: 'Too many confirmation emails. Please wait before trying again.',
        }
      }
      throw error
    }
 
    return { success: true }
  } catch (error) {
    console.error('Email send failed:', error)
    return { success: false, error }
  }
}

Solution 5: Resend Confirmation Email#

Allow users to resend confirmation:

typescript
// app/api/resend-confirmation/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
 
export async function POST(request: Request) {
  const { email } = await request.json()
  const cookieStore = await cookies()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          )
        },
      },
    }
  )
 
  const { error } = await supabase.auth.resend({
    type: 'signup',
    email,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })
 
  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 })
  }
 
  return NextResponse.json({ message: 'Confirmation email sent' })
}

Frontend Component#

typescript
'use client'
 
import { useState } from 'react'
 
export function ResendConfirmation({ email }: { email: string }) {
  const [loading, setLoading] = useState(false)
  const [message, setMessage] = useState('')
 
  async function handleResend() {
    setLoading(true)
    
    const response = await fetch('/api/resend-confirmation', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    })
 
    const data = await response.json()
    setMessage(data.message || data.error)
    setLoading(false)
  }
 
  return (
    <div>
      <p>Didn't receive the email?</p>
      <button onClick={handleResend} disabled={loading}>
        {loading ? 'Sending...' : 'Resend confirmation email'}
      </button>
      {message && <p>{message}</p>}
    </div>
  )
}

Common Mistakes#

  • Mistake #1: Not checking spam folder - Always check spam/junk folders first

  • Mistake #2: Using default Supabase email - Configure custom SMTP for reliability

  • Mistake #3: Wrong redirect URL - Ensure Site URL matches your domain exactly

  • Mistake #4: Missing DNS records - SPF/DKIM required for production

  • Mistake #5: Not handling errors - Always show user-friendly error messages

Conclusion#

Email confirmation issues in Supabase are usually caused by SMTP configuration, template problems, or DNS settings. The most reliable solution is configuring custom SMTP with a provider like SendGrid or Resend, along with proper DNS records for production.

Follow the solutions above in order: configure SMTP first, then fix templates, add DNS records, and implement resend functionality. Test thoroughly in development before deploying to production.

Monitor your email deliverability using services like mail-tester.com and always provide users with a way to resend confirmation emails if needed.

Further reading: Supabase AuthSessionMissingError in Middleware: 5 Fixes. Also see Fix Supabase: Invalid Refresh Token Already Used.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.