DEV Community

Cover image for Supabase Auth Hooks Explained: Blocking Temporary Domains Before User Creation
VTPShopy
VTPShopy

Posted on

Supabase Auth Hooks Explained: Blocking Temporary Domains Before User Creation

Supabase has rapidly emerged as one of the premier open-source alternatives to Firebase, offering developers a full PostgreSQL database, instantaneous real-time subscriptions, edge functions, and an integrated authentication suite powered by GoTrue.

However, just like any authentication provider, Supabase applications are vulnerable to bot networks, free-trial abusers, and malicious actors who leverage disposable, temporary, and burner email addresses to flood your database.

If you are building a B2B SaaS platform or consumer web app, allowing temporary emails (like @temp-mail.org or @10minutemail.com) to bypass your registration form introduces severe technical debt. It inflates database storage, skews analytics, consumes third-party API quotas, and destroys your domain's email deliverability rates when automated onboarding sequences generate hard bounces.

In this deep-dive technical guide, we will explore Supabase Auth Hooks (specifically the before-user-created hook) and demonstrate how to intercept disposable emails using an Edge Function paired with a real-time validation API before a database row is ever inserted.


Part 1: Why Traditional Client-Side and RLS Validation Fails

When developers notice fake signups polluting their Supabase database, they usually try one of two inadequate solutions:

1. Client-Side Regex Validation

Writing a validation function in your React, Vue, or Svelte frontend to check an email string against a static blocklist of known disposable domains is fundamentally flawed.

  • Easily Bypassed: Malicious actors can inspect your client-side JavaScript bundle, find your regex or array of blocked domains, and bypass the check using a newly registered temporary domain.
  • Obsolete Data: Temporary email services purchase and rotate thousands of new, obscure domains daily. A hardcoded list compiled into your frontend becomes obsolete almost immediately.

2. PostgreSQL Triggers and Row Level Security (RLS)

PostgreSQL supports powerful BEFORE INSERT triggers on tables. Some developers attempt to write a PL/pgSQL function attached to the auth.users table to validate the incoming email.

While this runs on the server side, PostgreSQL is not natively designed to make low-latency, synchronous HTTP calls to third-party threat-intelligence APIs during an insertion event. Attempting to make outbound HTTP requests from inside a database trigger introduces massive latency, locks database transactions, and risks crashing your database pool if an external API experiences slowdowns.


Part 2: The Solution – The before-user-created Auth Hook

Supabase solves this architectural problem elegantly with Auth Hooks.

Auth Hooks allow developers to execute custom logic during specific stages of the authentication lifecycle. Instead of running logic on the client or inside a heavy database trigger, Supabase routes the event payload to an HTTP endpoint—such as a Supabase Edge Function—which can perform async operations and approve or deny the authentication event.

The Lifecycle of before-user-created

  1. The Trigger: A user submits their credentials via supabase.auth.signUp().
  2. The Interception: Before inserting the new record into the auth.users table, Supabase pauses the execution and fires an HTTPS request containing the user payload to your before-user-created Auth Hook.
  3. Real-Time API Validation: The Edge Function extracts the user's email and pings a dynamic email validation engine like MailCheck.
  4. The Verdict:
  5. If Disposable/Malicious: The Edge Function returns an HTTP 400 Bad Request or custom error JSON. Supabase aborts the sign-up process entirely. No row is inserted in auth.users, no profile trigger fires, and no verification email is dispatched.
  6. If Clean: The Edge Function returns a success response. Supabase proceeds to insert the user into auth.users.

Part 3: The Validation Engine – MailCheck

To ensure that pausing the sign-up event doesn't introduce friction for legitimate users, your validation check must be fast. If your validation endpoint takes 3 seconds to respond, users will assume your registration form is broken and abandon your site.

For this tutorial, we will use the MailCheck API. Engineered specifically for real-time API interception, MailCheck maintains a continuously updated registry of over 40 million disposable and high-risk domains, delivering sub-50ms average latency. By querying MailCheck from inside your Supabase Auth Hook, you can verify emails instantly without degrading the signup experience. You can review the complete integration specs in the MailCheck API documentation.


Part 4: Step-by-Step Implementation

Now, let's write the code to secure your Supabase application.

Step 1: Create the Supabase Edge Function

Make sure you have the Supabase CLI installed and linked to your project. Run the following command in your terminal to create a new Edge Function named validate-email-hook:

supabase functions new validate-email-hook

Enter fullscreen mode Exit fullscreen mode

This creates a new folder at supabase/functions/validate-email-hook/index.ts.

Step 2: Implement the Edge Function Code

Open supabase/functions/validate-email-hook/index.ts and add the following TypeScript code. Supabase Edge Functions run on Deno, allowing high-speed execution at the edge.

// supabase/functions/validate-email-hook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";

interface AuthHookPayload {
  event: {
    type: string;
  };
  user: {
    id: string;
    email: string;
    user_metadata: Record<string, unknown>;
  };
}

serve(async (req: Request) => {
  try {
    // 1. Parse the incoming hook payload sent by Supabase Auth
    const payload: AuthHookPayload = await req.json();
    const userEmail = payload.user?.email;

    if (!userEmail) {
      return new Response(
        JSON.stringify({
          error: {
            message: "Email address is required for registration.",
          },
        }),
        { status: 400, headers: { "Content-Type": "application/json" } }
      );
    }

    // 2. Fetch the MailCheck API key securely from environment variables
    const mailcheckApiKey = Deno.env.get("MAILCHECK_API_KEY");
    if (!mailcheckApiKey) {
      console.error("Missing MAILCHECK_API_KEY environment variable.");
      // Fail open to avoid blocking users if configuration is missing
      return new Response(JSON.stringify({}), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    }

    // 3. Call the MailCheck API to validate the email
    const mailcheckUrl = `https://api.mailcheck.fadsync.com/v1/validate?email=${encodeURIComponent(userEmail)}`;
    const apiResponse = await fetch(mailcheckUrl, {
      method: "GET",
      headers: {
        "Authorization": `Bearer ${mailcheckApiKey}`,
        "Content-Type": "application/json",
      },
    });

    if (apiResponse.ok) {
      const data = await apiResponse.json();

      // 4. Check if the domain is disposable or flagged as high-risk
      if (data.is_disposable) {
        console.warn(`Blocked disposable email signup attempt: ${userEmail}`);

        // Returning a 4xx error causes Supabase Auth to reject the user creation
        return new Response(
          JSON.stringify({
            error: {
              http_code: 400,
              message: "Temporary and disposable email addresses are not allowed. Please use a valid personal or business email.",
            },
          }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }
    } else {
      console.error(`MailCheck API error response: ${apiResponse.status}`);
      // Fail open on external API errors (like rate limits) to preserve UX
    }

    // 5. Email is clean. Return an empty 200 OK response to allow user creation.
    return new Response(JSON.stringify({}), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });

  } catch (error) {
    console.error("Error in validate-email-hook:", error);
    // Fail open on unexpected exceptions
    return new Response(JSON.stringify({}), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  }
});

Enter fullscreen mode Exit fullscreen mode

Step 3: Set Secrets and Deploy the Edge Function

Set your MailCheck API Key in your Supabase project secrets using the CLI:

supabase secrets set MAILCHECK_API_KEY=your_actual_mailcheck_api_key

Enter fullscreen mode Exit fullscreen mode

Now, deploy the Edge Function:

supabase functions deploy validate-email-hook --no-verify-jwt

Enter fullscreen mode Exit fullscreen mode

Note: Save the deployed Function URL provided by the CLI (e.g., [https://your-project-ref.supabase.co/functions/v1/validate-email-hook](https://your-project-ref.supabase.co/functions/v1/validate-email-hook)).

Step 4: Configure the Auth Hook in Supabase Dashboard

  1. Log into your Supabase Dashboard.
  2. Navigate to Authentication -> Hooks.
  3. Under Before User Created (or before-user-created), click Add Hook.
  4. Set the hook type to HTTPS Edge Function.
  5. Paste your deployed Edge Function URL ([https://your-project-ref.supabase.co/functions/v1/validate-email-hook](https://your-project-ref.supabase.co/functions/v1/validate-email-hook)).
  6. Click Save.

Part 5: Handling Edge Cases and Production Resilience

When integrating third-party APIs into mission-critical auth flows, you must build for resilience.

Graceful Degradation (Failing Open)

Notice how in the Edge Function code above, if the fetch to MailCheck fails (due to a network glitch or a 429 Too Many Requests status code), the function logs the event and returns a 200 OK response.

This is an architectural best practice known as Failing Open. In high-converting applications, it is better to occasionally let a temporary email slip through during a rare API outage than to block legitimate human users from signing up. If you are handling high traffic volume, reference the guide on how to handle 429 Too Many Requests to optimize your rate-limiting strategies.

Protecting Your Payment Pipeline

If your Supabase database triggers an automated workflow that creates a customer in Stripe upon signup, blocking temporary emails at the Auth Hook level is even more vital. Preventing bad actors at the top of the funnel ensures fake users never pollute your subscription analytics. For further details on securing payment workflows, read how to prevent free trial abuse on Stripe and SaaS platforms.


Conclusion

By pairing Supabase Auth Hooks with a high-speed email validation service like MailCheck, you create an impenetrable, sub-50ms security perimeter for your application.

Instead of dealing with polluted PostgreSQL tables, hard-bounced marketing emails, and inflated cloud infrastructure bills, your database will contain only real, verified users. Implementing the before-user-created hook ensures that temporary domains are caught and rejected at the front door, leaving your core architecture clean, secure, and ready to scale.

Top comments (0)