Building a secure authentication system is not just about strong passwords. It starts with checking the quality of user signups in your Supabase.com project. Disposable email addresses often lead to spam accounts, abuse, and fake users that fill up your database. Also, Gmail's flexible rules (like dots and +tags) let people make many accounts from one email.
Meet email_guard, a Trusted Language Extension (TLE) for PostgreSQL. It works well with Supabase Auth hooks to:
- Block disposable email domains with a big, auto-updated list.
- Normalize Gmail addresses to stop duplicate signups (by removing dots and +tags).
- Offer easy installation that fits in any schema.
When you use this with Supabase's own security tools, like leaked password protection, you build strong protection against bad signups and abuse.
Why You Need email_guard for Supabase Authentication Security
The Problem: Disposable Emails and Gmail Tricks
Disposable email services (like mailinator.com or guerrillamail.com) let anyone make short-term emails. These are good for tests, but often used to:
- Make spam or bot accounts.
- Skip limits on trials or rates.
- Avoid bans by making new accounts fast.
Gmail addressing quirks are tricky too. These all go to the same inbox:
Without fixing this, a user can make many accounts by adding dots or +tags. This breaks rules for one account per person and can cheat referral programs or trials.
The Solution: Smart Email Validation at Signup in Supabase
The email_guard TLE gives you:
- A blocklist for disposable domains with over 20,000 known ones (updated weekly).
- Gmail normalization that removes dots and +tags, and sets the domain to gmail.com.
- A helper for Supabase Auth hooks that checks before creating a user.
All this runs in PostgreSQL, so it is fast, safe, and clear. For more on Supabase auth, see the Supabase Auth docs.
Understanding Trusted Language Extensions (TLE) in Supabase
Before installation, let's quickly explain what a Trusted Language Extension is and why it helps.
What is a TLE?
A Trusted Language Extension (TLE) is a PostgreSQL add-on written in a safe language (like PL/pgSQL or PL/Python). You can install it without special admin rights. This is key for hosted setups like Supabase, where you lack full access.
TLEs come from database.dev, the package manager for PostgreSQL. This makes them:
- Easy to install with the Supabase CLI.
- Version-controlled for safe updates.
- Flexible to place in any schema.
- Safe for production.
Learn more in Supabase's Trusted Language Extensions for Postgres blog post or the extensions docs.
Why Use TLEs for Security in Supabase?
With security logic in a TLE:
- It runs in the database, near your data.
- It stays the same for all apps and calls.
- You can check it with version history.
- It cannot be skipped by app code.
This is great for auth checks, data rules, and policies.
Step 1: Installing the TLE Infrastructure on Supabase
Before adding email_guard, set up the pg_tle extension (already on Supabase) and the dbdev tool for easy install.
A. Set Up dbdev and Supabase CLI (If Needed)
If not done yet:
- Install dbdev CLI: Follow the dbdev getting started guide.
- Install Supabase CLI: Check the Supabase CLI docs.
- Link your project: Run supabase link to connect to your Supabase database.
Supabase has pg_tle ready, so create it:
CREATE EXTENSION IF NOT EXISTS pg_tle;B. Generate the Migration with dbdev
Use dbdev to get the latest email_guard (version 0.3.1 or newer) in your migrations:
dbdev add \ -o ./supabase/migrations/ \ -v 0.3.1 \ -s extensions \ package \ -n mansueli@email_guardWhat this does:
- Makes a new migration file in ./supabase/migrations/.
- Puts the extension in the extensions schema (good for Supabase).
- Uses version 0.3.1 (change for new versions).
Adjust the -o path if your folder is different.
C. Apply the Migration
Push to your Supabase database:
supabase db pushDone! The extension is installed with the full blocklist.
For more on managing extensions, see Supabase database extensions docs.
Step 2: Understanding What You Just Installed
The email_guard extension adds objects in your schema (like extensions):
Table: disposable_email_domains
CREATE TABLE extensions.disposable_email_domains ( domain text PRIMARY KEY, CONSTRAINT disposable_email_domains_domain_lowercase CHECK (domain = lower(domain)) );It fills with domains like:
- mailinator.com
- guerrillamail.com
- 10minutemail.com
- And many more.
Function: normalize_email(text)
SELECT extensions.normalize_email('[email protected]');What it does:
- Makes lowercase.
- For Gmail/Googlemail:
- Removes dots from the start.
- Cuts after + (and the +).
- Sets domain to gmail.com.
- For others: Just lowercase.
Function: is_disposable_email_domain(text)
SELECT extensions.is_disposable_email_domain('mailinator.com'); SELECT extensions.is_disposable_email_domain('gmail.com');Smart check:
- Looks at parent domains (e.g., sub.mailinator.com matches).
- Fast with index.
Function: is_disposable_email(text)
SELECT extensions.is_disposable_email('[email protected]');Hook Helper: hook_prevent_disposable_and_enforce_gmail_uniqueness(jsonb)
This is the key part! For Supabase Auth hooks, it:
- Checks disposable domains → Sends 403 error if yes.
- Normalizes Gmail → Checks if the same normalized email exists.
- Sends 409 error if duplicate.
- Allows phone signups or non-email.
Step 3: Wire Up the Supabase Auth Hook for Email Validation
Now, connect it to signups.
Navigate to Auth Hooks in Dashboard
- Go to your Supabase Dashboard.
- Pick your project.
- Go to Authentication → Hooks.
- Turn on Before User Created.
Configure the Hook
Pick:
- Hook Type: Postgres Function.
- Schema: extensions (or your choice).
- Function: hook_prevent_disposable_and_enforce_gmail_uniqueness.

Done! The hook is on. See Supabase Auth Hooks docs for details.
What Happens During Signup
When signing up, the hook checks first:
supabase.auth.signUp({ email: '[email protected]', password: 'secure_password_123' }) supabase.auth.signUp({ email: '[email protected]', password: 'secure_password_123' }) supabase.auth.signUp({ email: '[email protected]', password: 'secure_password_123' })Step 4: Testing Your email_guard Setup on Supabase
Check if it works.
Test 1: Check Disposable Email Detection
SELECT extensions.is_disposable_email('[email protected]'); // False SELECT extensions.is_disposable_email('[email protected]');Test 2: Test Gmail Normalization
SELECT extensions.normalize_email('[email protected]'); SELECT extensions.normalize_email('[email protected]'); SELECT extensions.normalize_email('[email protected]');Test 3: Simulate the Hook
SELECT extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness( '{"user": {"email": "[email protected]"}}'::jsonb ); // Gmail duplicate (after adding test user) SELECT extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness( '{"user": {"email": "[email protected]"}}'::jsonb );Step 5: Combining with Supabase's Built-in Protections
email_guard is stronger with Supabase features.
Leaked Password Protection
Supabase checks against HaveIBeenPwned. Turn it on in Dashboard → Authentication → Password Protection.
supabase.auth.signUp({ email: '[email protected]', password: 'password123' })Keeping the Blocklist Current in email_guard
email_guard updates itself.
How Updates Work
A GitHub workflow:
- Runs every week (Mondays).
- Gets new list from disposable-email-domains repo.
- Makes upgrade script if changed.
- Updates version (e.g., 0.3.1 to 0.3.2).
- Saves changes auto.
Upgrading to the Latest Version
For new version:
dbdev add \ -o ./supabase/migrations/ \ -v 0.3.2 \ -s extensions \ package \ -n mansueli@email_guard supabase db pushIt adds new domains and keeps data. Watch releases on GitHub repo.
Advanced Usage & Customization for Supabase email_guard
Custom Domain Blocking
Block extra domains:
INSERT INTO extensions.disposable_email_domains (domain) VALUES ('suspicious-domain.com') ON CONFLICT DO NOTHING; DELETE FROM extensions.disposable_email_domains WHERE domain = 'some-domain.com';Checking Existing Users
Audit users:
SELECT id, email FROM auth.users WHERE extensions.is_disposable_email(email); WITH normalized AS ( SELECT id, email, extensions.normalize_email(email) AS normalized_email FROM auth.users WHERE email ILIKE '%@gmail.com' OR email ILIKE '%@googlemail.com' ) SELECT normalized_email, array_agg(email) AS duplicate_emails, count(*) AS duplicate_count FROM normalized GROUP BY normalized_email HAVING count(*) > 1;Custom Hook Logic
Make your own hook:
CREATE OR REPLACE FUNCTION public.my_custom_signup_hook(event jsonb) RETURNS jsonb LANGUAGE plpgsql AS $$ DECLARE user_email text; BEGIN user_email := event->'user'->>'email'; IF extensions.is_disposable_email(user_email) THEN RAISE EXCEPTION 'Nice try! No disposable emails here.' USING HINT = 'Please use a permanent email address', ERRCODE = 'P0001'; END IF; -- Add custom rules RETURN event; END; $$;Performance Considerations for email_guard in Supabase
Benchmarking
Functions are fast:
SELECT extensions.is_disposable_email_domain('mailinator.com'); SELECT extensions.normalize_email('[email protected]');Index Optimization
Extension adds index on auth.users(email). For big databases:
CREATE INDEX IF NOT EXISTS users_gmail_normalized_idx ON auth.users (extensions.normalize_email(email)) WHERE email ILIKE '%@gmail.com' OR email ILIKE '%@googlemail.com';Troubleshooting email_guard on Supabase
Hook Not Triggering
Check setup:
SELECT * FROM supabase_functions.hooks WHERE hook_name = 'before_user_created';Check rights:
GRANT EXECUTE ON FUNCTION extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(jsonb) TO supabase_auth_admin;False Positives
If wrong block:
DELETE FROM extensions.disposable_email_domains WHERE domain = 'legitimate-domain.com';Report to blocklist repo.
Migration Conflicts
Use different schema:
dbdev add \ -o ./supabase/migrations/ \ -v 0.3.1 \ -s email_guard \ package \ -n mansueli@email_guardUpdate hook to email_guard.hook_prevent_disposable_and_enforce_gmail_uniqueness.
Security Best Practices for Supabase Authentication
Defense in Depth
Add layers:
- Verify emails: Make users confirm.
- CAPTCHA: Use hCaptcha on forms.
- Rate limits: Stop many tries per IP.
- Review accounts: Flag odd patterns.
Monitoring
Track blocks:
CREATE TABLE IF NOT EXISTS blocked_signups ( id uuid DEFAULT gen_random_uuid() PRIMARY KEY, email text NOT NULL, reason text NOT NULL, created_at timestamptz DEFAULT now() );Privacy Considerations
- Avoid logging full emails.
- Hash data for stats.
- Follow GDPR/CCPA for stored info.
Conclusion: Building a Secure Foundation with email_guard on Supabase
Authentication is the door to your app. Secure it with layers. The email_guard TLE gives a simple way to block disposable emails and stop Gmail duplicates in Supabase.
With Supabase tools like leaked password checks, email verification, and rate limits, you get a strong system that:
- ✅ Blocks bad signups auto.
- ✅ Stops abuse without work.
- ✅ Grows with your app.
- ✅ Updates weekly.
It runs in the database, so it is clear, checked, and hard to skip.
Next Steps
- Install the extension as shown.
- Turn on the auth hook in dashboard.
- Test with disposable and Gmail tests.
- Watch logs for blocks.
- Update when new versions come.
For more, see:
- email_guard GitHub repository
- Supabase Auth Hooks documentation
- Trusted Language Extensions blog post
- database.dev package registry
Check my previous posts: Building User Authentication with Username and Password Using Supabase and Streamlining PostgreSQL Function Management with Supabase.
.png)


