How to validate signups with Supabase Auth (with code, 2026) — SignupDoggy Blog

A working Supabase Edge Function that scores a signup before allowing the user to be created. Wires SignupDoggy into the Supabase Auth sign-up trigger, so disposable emails, VPN users, and bots never reach your profiles table.

How to validate signups with Supabase Auth (with code, 2026)

Supabase Auth is the most popular auth provider for indie hackers in 2026. It is fast, it is free up to 50,000 monthly active users, and it integrates with everything. The one thing it does not do out of the box: validate that the user signing up is a real human with a real email, not a bot with a `tempmail.com` address.

This post is the working code for adding signup validation to Supabase Auth. Two approaches: a database trigger (recommended) and an Edge Function (alternative). Both work, both take 30 minutes to implement.

Short answer

A Postgres trigger on `auth.users` that calls the SignupDoggy API via the `http` extension. If the API returns `recommendation: 'block'`, the trigger raises an exception and the user is not created. The profiles table never gets the row. Your database stays clean.

The Edge Function approach: wrap the signup in a Supabase Edge Function that calls SignupDoggy before delegating to `supabase.auth.signUp()`. More moving parts, but easier to debug.

For 90% of Supabase users, the database trigger is the right answer.

The database trigger approach

Step 1: enable the http extension

create extension if not exists http;

The `http` extension is built into Supabase. It lets you make HTTP calls from inside a Postgres function. It is the right tool for this job.

Step 2: create the validation function

create or replace function public.checksignupquality()
returns trigger
language plpgsql
security definer
as $
declare
  result jsonb;
  recommendation text;
  requestid uuid;
  userip text;
begin
  -- Get the user's IP from the request headers
  userip := currentsetting('request.headers', true)::json->>'x-forwarded-for';

  -- Make the API call
  select body into result
  from httppost(
    'https://signupdoggy-api.jeffrinjames99.workers.dev/v1/check',
    jsonbbuildobject(
      'email', new.email,
      'ip', coalesce(userip, '0.0.0.0')
    ),
    'application/json',
    jsonbbuildobject(
      'X-API-KEY', currentsetting('app.signupdoggykey', true)
    )
  );

  recommendation := result->>'recommendation';

  -- Block the signup if the recommendation is 'block'
  if recommendation = 'block' then
    raise exception 'Signup blocked: high risk signal (%)', result->>'overallrisk';
  end if;

  return new;
end;
$;

Step 3: wire it up to the auth.users table

create trigger checksignupqualitytrigger
  before insert on auth.users
  for each row execute function public.checksignupquality();

The trigger fires before the row is inserted. If the function raises an exception, the insert is rolled back. The user is not created.

Step 4: set the API key

alter database postgres set app.signupdoggykey = 'sdyourkeyhere';

In production, use a Supabase secret instead:

-- Run this from the Supabase dashboard SQL editor with a service-role key
alter database postgres set app.signupdoggykey = 'sdyourkeyhere';

Step 5: handle the 'review' recommendation

The `review` recommendation means 'this user is suspicious but not clearly a bot.' The right behavior: create the user, but mark them for manual review.

-- In the function:
if recommendation = 'review' then
  -- Insert a flag into a separate table
  insert into public.signupreviewqueue (userid, riskdata)
  values (new.id, result);
end if;

return new;  -- allow the signup

Then in your admin dashboard, surface the `signupreviewqueue` table. Manual reviewers can see the suspicious signups and take action (delete, ask for ID, allow).

The Edge Function approach

If you prefer not to use Postgres triggers (some teams don't, for debugging reasons), the alternative is a Supabase Edge Function.

// supabase/functions/signup/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const { email, password, ip } = await req.json();

  // Call the fraud API
  const fraudResult = await fetch('https://signupdoggy-api.jeffrinjames99.workers.dev/v1/check', {
    method: 'POST',
    headers: {
      'X-API-KEY': Deno.env.get('SIGNUPDOGGYKEY')!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, ip }),
  }).then(r => r.json());

  if (fraudResult.recommendation === 'block') {
    return new Response(
      JSON.stringify({ error: 'Signup blocked' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Delegate to Supabase Auth
  const supabaseAdmin = createClient(
    Deno.env.get('SUPABASEURL')!,
    Deno.env.get('SUPABASESERVICEROLEKEY')!,
  );

  const { data, error } = await supabaseAdmin.auth.admin.createUser({
    email,
    password,
    emailconfirm: true,  // skip email confirmation
  });

  if (error) {
    return new Response(JSON.stringify({ error: error.message }), { status: 400 });
  }

  if (fraudResult.recommendation === 'review') {
    await supabaseAdmin.from('signupreviewqueue').insert({
      userid: data.user.id,
      riskdata: fraudResult,
    });
  }

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

Then in your client code, call the Edge Function instead of `supabase.auth.signUp()`:

// Frontend
const { data, error } = await supabase.functions.invoke('signup', {
  body: { email, password, ip: clientIp },
});

The Edge Function approach has more moving parts but is easier to debug — you can add console.log statements, and the function is isolated from your database triggers.

Which approach to use

Database trigger (recommended for most teams):
• Pros: zero client-side changes, runs server-side, impossible to bypass from the client
• Cons: harder to debug, requires the `http` extension, requires a service-role key in the database settings

Edge Function (recommended for teams that want more control):
• Pros: easier to debug, easier to version control, can be called from non-Supabase clients
• Cons: requires changes to the client code, requires a Supabase Edge Function deployment

For a 2-person team shipping a SaaS, the database trigger is the right answer. For a larger team with more complex requirements, the Edge Function approach gives you more flexibility.

The IP detection gotcha

The database trigger approach above tries to get the user's IP from the `request.headers` setting. This works if you set the header in your Supabase client's auth options, but not by default.

The cleanest way to pass the IP to the trigger:

// Frontend
const { data, error } = await supabase.auth.signUp({
  email,
  password,
  options: {
    data: {
      ip: await getClientIp(),  // fetch from /api/my-ip or similar
    },
  },
});

Then in the trigger, get the IP from `new.rawusermetadata->>'ip'`.

Alternatively, use Supabase's `request.headers` setting if you've configured it (this is a Supabase-specific feature that requires a custom claim).

The cost

SignupDoggy charges $0.01 per call. At 1,000 signups per month, that's $10. At 10,000 signups per month, that's $100. The trigger only fires for new signups, not for logins. The cost scales with your actual signup volume.

FAQ

Q: Will the trigger slow down signup?
A: The API call adds ~50ms to the signup time. Acceptable for a user-facing signup form.

Q: What if the SignupDoggy API is down?
A: The trigger will fail. The signup will not be created. For high-availability requirements, add a fallback: if the API returns 5xx, allow the signup but mark for review.

Q: Can I test the trigger locally?
A: Yes. Run the trigger function in a SQL query with mock data. Supabase Studio has a SQL editor that supports this.

Q: What about Supabase Auth webhooks?
A: Supabase has a `before-user-created` webhook hook you can use. It runs in a serverless function and is more flexible than a database trigger. The trade-off is added complexity.

Q: Does this work with Supabase's built-in rate limiting?
A: Yes. The trigger runs after rate limiting. If a user is rate-limited, they never reach the trigger.

---

About the author

Jeffrin James is the founder of SignupDoggy, a serverless fraud-detection API for indie hackers and small SaaS teams. He built the product in Mumbai, India, after spending six months and $2,400 on enterprise fraud-detection vendors that didn't fit his use case.

Tags:** Supabase, Tutorial, Integration, Auth