# How to Validate Email Format in Supabase Auth

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

## TL;DR

Supabase Auth does not enforce strict email format validation beyond basic checks. To prevent malformed emails, add client-side validation with a regex pattern before calling signUp, and add server-side validation with a PostgreSQL trigger or Edge Function that checks email format on the auth.users table. This ensures only properly formatted email addresses can register, reducing bounce rates and invalid accounts.

## Adding Email Format Validation to Supabase Auth

Supabase Auth accepts any string that has an @ symbol as an email address. This means addresses like 'test@' or 'user@.com' can pass through. For production apps, you need stricter validation to ensure email deliverability. This tutorial covers three layers of defense: client-side validation in your signup form, a database trigger that rejects malformed emails before they are stored, and an optional Edge Function for advanced validation like disposable email detection.

## Before you start

- A Supabase project with email/password auth enabled
- Access to the SQL Editor in the Supabase Dashboard
- @supabase/supabase-js installed in your frontend project
- Basic understanding of regular expressions

## Step-by-step guide

### 1. Add client-side email validation before signUp

The first layer of defense is validating the email format in your frontend before making the API call. Use a regex pattern that checks for a valid email structure: local part, @ symbol, domain, and TLD. Also use the HTML5 type='email' attribute on input fields for basic browser validation. Client-side validation provides instant feedback and prevents unnecessary API calls, but it can be bypassed, so it must be paired with server-side validation.

```
// Email validation utility
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

function isValidEmail(email: string): boolean {
  if (!email || email.length > 254) return false
  return EMAIL_REGEX.test(email)
}

// Usage in signup handler
async function handleSignup(email: string, password: string) {
  if (!isValidEmail(email)) {
    return { error: 'Please enter a valid email address.' }
  }

  const { data, error } = await supabase.auth.signUp({
    email: email.toLowerCase().trim(),
    password,
  })

  return { data, error }
}
```

**Expected result:** Malformed email addresses are rejected instantly in the UI before making any API call to Supabase.

### 2. Create a PostgreSQL function for server-side email validation

Client-side validation can be bypassed by calling the API directly. Add a database trigger that validates email format on every INSERT into auth.users. Create a PL/pgSQL function that checks the email against a regex pattern and raises an exception if it does not match. This is the strongest form of validation because it runs at the database level regardless of how the request arrives.

```
-- Create the validation function
create or replace function public.validate_email_format()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  -- Check email format with regex
  if new.email !~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' then
    raise exception 'Invalid email format: %', new.email;
  end if;

  -- Check email length (RFC 5321 limit)
  if length(new.email) > 254 then
    raise exception 'Email address exceeds maximum length of 254 characters';
  end if;

  return new;
end;
$$;

-- Create the trigger on auth.users
create trigger validate_email_before_insert
  before insert on auth.users
  for each row
  execute function public.validate_email_format();
```

**Expected result:** Any attempt to insert a user with a malformed email raises a database exception, which Supabase returns as an error to the client.

### 3. Handle validation errors in the frontend

When the database trigger rejects an email, Supabase returns an error with the exception message. Catch this error in your signup handler and display a user-friendly message. Map specific error patterns to helpful messages so users understand what went wrong and how to fix it.

```
async function handleSignup(email: string, password: string) {
  // Client-side validation first
  if (!isValidEmail(email)) {
    return { error: 'Please enter a valid email address (e.g., name@example.com).' }
  }

  const { data, error } = await supabase.auth.signUp({
    email: email.toLowerCase().trim(),
    password,
  })

  if (error) {
    // Handle server-side validation errors
    if (error.message.includes('Invalid email format')) {
      return { error: 'The email address format is not valid. Please check and try again.' }
    }
    if (error.message.includes('already registered')) {
      return { error: 'An account with this email already exists. Try logging in instead.' }
    }
    return { error: 'Signup failed. Please try again later.' }
  }

  return { data, error: null }
}
```

**Expected result:** Users see a clear, friendly error message when their email format is rejected by either the client-side or server-side validation.

### 4. Add an Edge Function for advanced validation (optional)

For advanced validation like blocking disposable email providers, checking MX records, or enforcing domain allowlists, create an Edge Function that acts as a signup proxy. The function validates the email, then calls supabase.auth.admin.createUser() with the service role key if validation passes. This approach gives you full control over the signup flow while keeping the validation logic server-side.

```
// supabase/functions/validated-signup/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'

const BLOCKED_DOMAINS = ['tempmail.com', 'throwaway.email', 'mailinator.com']
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  const { email, password } = await req.json()
  const normalizedEmail = email.toLowerCase().trim()

  // Validate format
  if (!EMAIL_REGEX.test(normalizedEmail)) {
    return new Response(
      JSON.stringify({ error: 'Invalid email format' }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  // Check for disposable email domains
  const domain = normalizedEmail.split('@')[1]
  if (BLOCKED_DOMAINS.includes(domain)) {
    return new Response(
      JSON.stringify({ error: 'Disposable email addresses are not allowed' }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  // Proceed with signup using the regular client
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!
  )

  const { data, error } = await supabase.auth.signUp({
    email: normalizedEmail,
    password,
  })

  if (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }

  return new Response(
    JSON.stringify({ data }),
    { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  )
})
```

**Expected result:** The Edge Function validates email format and blocks disposable providers before creating the account. Invalid signups are rejected with clear error messages.

### 5. Test the validation with edge cases

Test your validation layers with emails that should pass and emails that should fail. Include edge cases like emails with special characters in the local part, very long addresses, addresses with subdomains, and common typos. Verify that both the client-side validation and the database trigger produce appropriate error messages.

```
// Test cases for email validation
const testEmails = [
  // Should PASS
  { email: 'user@example.com', expected: true },
  { email: 'user.name+tag@domain.co.uk', expected: true },
  { email: 'user@sub.domain.com', expected: true },

  // Should FAIL
  { email: 'user@', expected: false },
  { email: '@domain.com', expected: false },
  { email: 'user@.com', expected: false },
  { email: 'user@domain', expected: false },
  { email: 'user domain@test.com', expected: false },
  { email: '', expected: false },
  { email: 'a'.repeat(255) + '@test.com', expected: false },
]

for (const { email, expected } of testEmails) {
  const result = isValidEmail(email)
  console.log(`${email}: ${result === expected ? 'PASS' : 'FAIL'}`)
}
```

**Expected result:** All valid emails pass validation and all malformed emails are rejected at both the client and server level.

## Complete code example

File: `validate-email.sql`

```sql
-- ============================================
-- Email Format Validation for Supabase Auth
-- Run in Supabase SQL Editor
-- ============================================

-- Create the email validation function
create or replace function public.validate_email_format()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
declare
  email_pattern text := '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
begin
  -- Normalize the email
  new.email := lower(trim(new.email));

  -- Check email is not empty
  if new.email is null or new.email = '' then
    raise exception 'Email address is required';
  end if;

  -- Check email length (RFC 5321)
  if length(new.email) > 254 then
    raise exception 'Email address exceeds maximum length';
  end if;

  -- Check format with regex
  if new.email !~ email_pattern then
    raise exception 'Invalid email format';
  end if;

  -- Check local part length (before @)
  if length(split_part(new.email, '@', 1)) > 64 then
    raise exception 'Email local part exceeds maximum length';
  end if;

  return new;
end;
$$;

-- Create trigger on auth.users for new signups
create trigger validate_email_before_insert
  before insert on auth.users
  for each row
  execute function public.validate_email_format();

-- Optional: Also validate on email updates
create trigger validate_email_before_update
  before update of email on auth.users
  for each row
  when (old.email is distinct from new.email)
  execute function public.validate_email_format();
```

## Common mistakes

- **Relying only on client-side validation, which can be bypassed by calling the API directly** — undefined Fix: Always pair client-side validation with server-side enforcement via a database trigger or Edge Function. Client-side validation is for UX; server-side validation is for security.
- **Using an overly strict regex that rejects valid email addresses with + signs, dots, or subdomains** — undefined Fix: Test your regex with valid edge cases like user.name+tag@sub.domain.co.uk. The pattern ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ covers most valid formats.
- **Not normalizing email addresses, allowing duplicate accounts with different casing or whitespace** — undefined Fix: Call .toLowerCase().trim() on the email in your client code and in the database trigger. This prevents 'User@Example.com' and 'user@example.com' from being separate accounts.

## Best practices

- Validate email format on the client for instant UX feedback and on the server for security enforcement
- Normalize emails with toLowerCase() and trim() before validation and storage to prevent duplicates
- Use a database trigger on auth.users for the strongest server-side enforcement that cannot be bypassed
- Keep the email regex reasonably permissive — overly strict patterns reject valid addresses
- Check email length limits: 254 characters total, 64 characters for the local part (before @)
- Never expose raw database error messages to users — map them to friendly, helpful messages
- Consider blocking disposable email providers via an Edge Function for apps that require real email addresses

## Frequently asked questions

### Does Supabase validate email format by default?

Supabase performs minimal validation — it checks that the string contains an @ symbol but does not enforce a strict format. Addresses like 'test@' or 'user@.com' may pass through. Add your own validation for stricter enforcement.

### Can I block disposable email addresses in Supabase?

Yes. Create an Edge Function that checks the email domain against a blocklist of known disposable email providers. Route your signup flow through this function instead of calling supabase.auth.signUp directly.

### Will the database trigger affect OAuth signups?

Yes, the trigger runs on every INSERT into auth.users, including OAuth signups. Since OAuth providers like Google and GitHub provide verified email addresses, they should always pass format validation.

### What regex should I use for email validation?

The pattern ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ covers the vast majority of valid email addresses. Avoid overly strict patterns that reject valid addresses with + signs or subdomains.

### How do I prevent duplicate accounts with different email casing?

Normalize emails to lowercase before passing them to signUp. Supabase stores emails case-sensitively by default. The database trigger can also normalize by setting new.email := lower(trim(new.email)).

### Can I add email validation without a database trigger?

Yes, you can validate in an Edge Function that acts as a signup proxy, or rely on client-side validation only. However, a database trigger is the most secure option because it cannot be bypassed regardless of how the API is called.

### Can RapidDev help set up robust email validation for my Supabase project?

Yes, RapidDev can implement multi-layer email validation including client-side checks, database triggers, disposable email blocking, and domain allowlists tailored to your application's requirements.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-validate-email-format-in-supabase-auth
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-validate-email-format-in-supabase-auth
