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:
john.doe@gmail.comj.o.h.n.d.o.e@gmail.comjohndoe+promo@gmail.comjohndoe+whatever@gmail.com
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 togmail.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 linkto 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_guard
What this does:
- Makes a new migration file in
./supabase/migrations/. - Puts the extension in the
extensionsschema (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 push
Done! 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
-- Holds over 20,000 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.comguerrillamail.com10minutemail.com- And many more.
Function: normalize_email(text)
-- Example: normalize_email('J.o.h.n.Doe+promo@gmail.com')
-- Returns: 'johndoe@gmail.com'
SELECT extensions.normalize_email('J.o.h.n.Doe+promo@gmail.com');
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)
-- Check if a domain is disposable
SELECT extensions.is_disposable_email_domain('mailinator.com'); -- true
SELECT extensions.is_disposable_email_domain('gmail.com'); -- false
Smart check:
- Looks at parent domains (e.g.,
sub.mailinator.commatches). - Fast with index.
Function: is_disposable_email(text)
-- Easy check for full emails
SELECT extensions.is_disposable_email('user@guerrillamail.com'); -- true
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:
// Try with disposable email
supabase.auth.signUp({
email: 'test@mailinator.com',
password: 'secure_password_123'
})
// ❌ Error: { message: "Disposable email addresses are not allowed", status: 403 }
// Try with duplicate Gmail
// If johndoe@gmail.com exists
supabase.auth.signUp({
email: 'j.o.h.n.d.o.e+test@gmail.com',
password: 'secure_password_123'
})
// ❌ Error: { message: "A user with this normalized email already exists", status: 409 }
// Valid email
supabase.auth.signUp({
email: 'alice@example.com',
password: 'secure_password_123'
})
// ✅ User created
Step 4: Testing Your email_guard Setup on Supabase
Check if it works.
Test 1: Check Disposable Email Detection
-- True
SELECT extensions.is_disposable_email('user@mailinator.com');
// False
SELECT extensions.is_disposable_email('user@gmail.com');
Test 2: Test Gmail Normalization
-- All return 'johndoe@gmail.com'
SELECT extensions.normalize_email('John.Doe@gmail.com');
SELECT extensions.normalize_email('j.o.h.n.d.o.e@googlemail.com');
SELECT extensions.normalize_email('johndoe+promo@gmail.com');
Test 3: Simulate the Hook
-- Disposable reject
SELECT extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(
'{"user": {"email": "test@guerrillamail.com"}}'::jsonb
);
-- Error: "Disposable email addresses are not allowed"
// Gmail duplicate (after adding test user)
SELECT extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(
'{"user": {"email": "j.o.h.n.d.o.e+test@gmail.com"}}'::jsonb
);
-- Error: "A user with this normalized email already exists"
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.
// Leaked password
supabase.auth.signUp({
email: 'alice@example.com',
password: 'password123' // Leaked
})
// ❌ Error: { message: "Password has been leaked", status: 422 }
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:
# Make upgrade migration
dbdev add \
-o ./supabase/migrations/ \
-v 0.3.2 \ # New
-s extensions \
package \
-n mansueli@email_guard
# Apply
supabase db push
It adds new domains and keeps data. Watch releases on GitHub repo.
Advanced Usage & Customization for Supabase email_guard
Custom Domain Blocking
Block extra domains:
-- Add one
INSERT INTO extensions.disposable_email_domains (domain)
VALUES ('suspicious-domain.com')
ON CONFLICT DO NOTHING;
-- Remove one
DELETE FROM extensions.disposable_email_domains
WHERE domain = 'some-domain.com';
Checking Existing Users
Audit users:
-- Find disposable
SELECT id, email
FROM auth.users
WHERE extensions.is_disposable_email(email);
-- Find Gmail duplicates
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:
-- Domain check: ~0.1ms
SELECT extensions.is_disposable_email_domain('mailinator.com');
-- Normalize: ~0.05ms
SELECT extensions.normalize_email('j.o.h.n.d.o.e+test@gmail.com');
-- Full hook: ~1-2ms
Index Optimization
Extension adds index on auth.users(email). For big databases:
-- Partial index for Gmail
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_guard
Update 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()
);
-- Log in hook (add yourself)
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.

Top comments (0)