# How to Implement Email Verification in Supabase

- Tool: Supabase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

To implement email verification in Supabase, enable the Confirm email setting in Authentication > Providers > Email in the Dashboard. When a user signs up, Supabase sends a confirmation email with a verification link. Handle the redirect callback in your app to complete verification, then check the user's email_confirmed_at field to determine if they have verified. Configure a custom SMTP provider to ensure reliable delivery beyond the default 2 emails per hour limit.

## Implementing Email Verification in Supabase Auth

Email verification ensures that users own the email address they sign up with. Supabase Auth has built-in support for email confirmation — when enabled, signUp returns a user object but no session until the user clicks the confirmation link. This tutorial covers the full flow from enabling the setting to handling the callback redirect.

## Before you start

- A Supabase project with Authentication enabled
- The Supabase JS client installed (@supabase/supabase-js v2+)
- A frontend application with routing (React Router, Next.js, etc.)
- A custom SMTP provider configured for production email delivery

## Step-by-step guide

### 1. Enable email confirmation in the Dashboard

Go to the Supabase Dashboard, navigate to Authentication > Providers, and click on the Email provider. Toggle on the Confirm email setting. When this is enabled, new signups will receive a confirmation email and their session will be null until they click the link. Also check the URL Configuration section under Authentication — set your Site URL to your production domain and add your app's callback URL to the Redirect URLs allowlist. The callback URL is where Supabase redirects users after they click the confirmation link.

**Expected result:** The Confirm email setting is enabled and your Site URL and Redirect URLs are configured.

### 2. Implement the signup flow with email confirmation

When you call signUp with Confirm email enabled, the user receives a confirmation email but the session will be null. Your UI should display a message telling the user to check their inbox. The emailRedirectTo option in the signUp call specifies where the user is sent after clicking the confirmation link. This URL must be in the Redirect URLs allowlist in the Dashboard.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

async function signUp(email: string, password: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback`,
    },
  })

  if (error) {
    console.error('Signup error:', error.message)
    return
  }

  // data.session is null when email confirmation is required
  if (data.user && !data.session) {
    console.log('Confirmation email sent. Check your inbox.')
  }
}
```

**Expected result:** The signup function sends a confirmation email and returns a user with a null session, prompting the user to check their inbox.

### 3. Handle the email confirmation callback

When the user clicks the confirmation link in their email, Supabase redirects them to your emailRedirectTo URL with authentication parameters in the URL hash. You need a callback route in your app that extracts these parameters and exchanges them for a session. For Next.js App Router, create a route handler. For React with React Router, create a callback page component. The key is to call supabase.auth.exchangeCodeForSession() if using PKCE flow, or let the client auto-detect the hash parameters.

```
// For Next.js App Router: app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get('code')
  const redirectTo = new URL('/', request.url)

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(redirectTo)
    }
  }

  // If there is no code or an error, redirect to an error page
  return NextResponse.redirect(new URL('/auth/error', request.url))
}
```

**Expected result:** The callback route exchanges the confirmation code for a session and redirects the verified user to the main app.

### 4. Check if a user has verified their email

After a user is authenticated, check the email_confirmed_at field on the user object to determine if they have verified their email. If this field is null, the user has not yet confirmed their email. Use this to conditionally show verification reminders, restrict access to certain features, or block certain actions until the email is confirmed.

```
async function isEmailVerified(): Promise<boolean> {
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) return false

  return user.email_confirmed_at !== null
}

// Usage in a component
const verified = await isEmailVerified()
if (!verified) {
  console.log('Please verify your email to continue.')
}
```

**Expected result:** Your app can check whether the current user has a verified email and display appropriate UI or restrictions.

### 5. Listen for the email verification event

Set up an onAuthStateChange listener to detect when a user confirms their email. The SIGNED_IN event fires when the user completes verification (either by clicking the link or entering an OTP). Use this to update your UI in real-time without requiring a page refresh. The USER_UPDATED event may also fire when email confirmation status changes.

```
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN' && session?.user.email_confirmed_at) {
    console.log('Email verified! User is now fully authenticated.')
    // Redirect to the main app or update UI state
  }
})
```

**Expected result:** The auth state listener detects when email verification is completed and triggers a UI update.

## Complete code example

File: `email-verification.ts`

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Sign up with email confirmation
async function signUpWithVerification(email: string, password: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback`,
    },
  })

  if (error) throw new Error(error.message)

  return {
    user: data.user,
    needsConfirmation: data.user && !data.session,
  }
}

// Check if current user has verified email
async function isEmailVerified(): Promise<boolean> {
  const { data: { user } } = await supabase.auth.getUser()
  return user?.email_confirmed_at !== null && user?.email_confirmed_at !== undefined
}

// Resend confirmation email
async function resendConfirmation(email: string) {
  const { error } = await supabase.auth.resend({
    type: 'signup',
    email,
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback`,
    },
  })

  if (error) throw new Error(error.message)
  return true
}

// Listen for verification completion
function onEmailVerified(callback: () => void) {
  const { data: { subscription } } = supabase.auth.onAuthStateChange(
    (event, session) => {
      if (event === 'SIGNED_IN' && session?.user.email_confirmed_at) {
        callback()
      }
    }
  )
  return subscription
}

// === Usage ===

// 1. Sign up
const result = await signUpWithVerification('user@example.com', 'password123')
if (result.needsConfirmation) {
  console.log('Check your inbox for a confirmation email.')
}

// 2. Listen for verification
const sub = onEmailVerified(() => {
  console.log('Email verified — redirecting to dashboard.')
})

// 3. Cleanup on unmount
// sub.unsubscribe()
```

## Common mistakes

- **Not adding the callback URL to the Redirect URLs allowlist in the Dashboard** — undefined Fix: Go to Authentication > URL Configuration and add your callback URL (e.g., https://yourapp.com/auth/callback) to the Redirect URLs list. Supabase blocks redirects to URLs not in the allowlist.
- **Assuming data.session is always available after signUp when Confirm email is enabled** — undefined Fix: When Confirm email is enabled, signUp returns a user but no session. Check for data.session === null and show a 'check your inbox' message instead of trying to access the session.
- **Using the default SMTP in production and hitting the 2 emails per hour rate limit** — undefined Fix: Configure a custom SMTP provider (Resend, SendGrid, Postmark) in Project Settings > Authentication > SMTP Settings before launching. The default 2 per hour limit is for development only.
- **Not handling the case where the confirmation link has expired** — undefined Fix: Confirmation links expire after 24 hours by default. If the user clicks an expired link, show an error message with a button to resend the confirmation email using supabase.auth.resend().

## Best practices

- Always configure a custom SMTP provider before going to production
- Set a clear emailRedirectTo URL that handles the authentication callback
- Show a helpful 'check your inbox' message after signup instead of a blank screen
- Provide a 'resend confirmation' button for users who do not receive the initial email
- Use getUser() on the server side to verify email_confirmed_at — never trust client-side session data for authorization
- Set up onAuthStateChange early in your app to catch verification events from redirects
- Add the confirmation callback URL to both development and production Redirect URLs lists

## Frequently asked questions

### How long is the email confirmation link valid?

By default, Supabase email confirmation links expire after 24 hours. If the link expires, the user needs to request a new confirmation email using supabase.auth.resend().

### What happens if a user signs up but never confirms their email?

The user record exists in auth.users but their email_confirmed_at field is null. They cannot sign in until they confirm their email. You can clean up unconfirmed users periodically using an admin script.

### Can I disable email confirmation for development?

Yes, go to Authentication > Providers > Email and toggle off Confirm email. This makes signUp return an active session immediately without sending a confirmation email. Re-enable it before deploying to production.

### Why is my confirmation email not being received?

Check three things: (1) the default SMTP has a 2 emails per hour limit — configure a custom SMTP provider; (2) the email may be in the spam folder; (3) verify the user's email address is correct in the auth.users table.

### Can I use a 6-digit OTP instead of a confirmation link?

Yes, Supabase supports OTP-based confirmation. Use the {{ .Token }} template variable in your email template to include the 6-digit code, and verify it client-side with supabase.auth.verifyOtp({ email, token, type: 'signup' }).

### Does email verification work with OAuth providers?

OAuth providers like Google and GitHub verify email addresses as part of their own flow. When a user signs in with OAuth, Supabase trusts the provider's email verification and sets email_confirmed_at automatically.

### Can RapidDev help implement a custom email verification flow for my Supabase project?

Yes, RapidDev can implement the full email verification flow including custom email templates, SMTP configuration, callback handling, and user experience optimizations like resend functionality and verification status indicators.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-implement-email-verification-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-implement-email-verification-in-supabase
