Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Allow Sign In Only with Specific Domains in Supabase

To restrict Supabase signups to specific email domains, create a PostgreSQL trigger on the auth.users table that checks the user's email domain during signup and raises an exception if the domain is not on the allowlist. Store allowed domains in a separate table for easy management. This approach works for all auth methods including email/password, magic link, and OAuth, because the trigger fires on every insert into auth.users.

What you'll learn

  • How to create a database trigger that blocks signups from unauthorized email domains
  • How to maintain an allowlist of approved domains in a database table
  • How to add client-side domain validation before calling signUp
  • How to handle rejected signups gracefully in your UI
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read15-20 minSupabase (all plans), @supabase/supabase-js v2+, PostgreSQL 15+March 2026RapidDev Engineering Team
TL;DR

To restrict Supabase signups to specific email domains, create a PostgreSQL trigger on the auth.users table that checks the user's email domain during signup and raises an exception if the domain is not on the allowlist. Store allowed domains in a separate table for easy management. This approach works for all auth methods including email/password, magic link, and OAuth, because the trigger fires on every insert into auth.users.

Restricting Signups to Specific Email Domains in Supabase

Many B2B applications need to restrict access to users from specific organizations — for example, only allowing signups from @yourcompany.com or @client.org. Supabase does not have a built-in domain allowlist feature, but you can implement one using a PostgreSQL trigger on the auth.users table. The trigger fires on every new signup and checks whether the user's email domain is in your approved list. If not, it blocks the signup. This tutorial shows you how to build and manage this restriction.

Prerequisites

  • A Supabase project with Auth enabled
  • Access to the SQL Editor in the Supabase Dashboard
  • A frontend app with an existing signup form
  • @supabase/supabase-js v2+ installed

Step-by-step guide

1

Create the allowed domains table

Create a table to store the email domains you want to allow. Using a table instead of hardcoding domains in the trigger function makes it easy to add or remove domains without modifying SQL functions. Enable RLS on this table and create a policy that allows only authenticated admin users to manage it. Public users should not be able to read or modify the allowed domains list.

typescript
1-- Create the allowed domains table
2CREATE TABLE public.allowed_email_domains (
3 id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
4 domain text NOT NULL UNIQUE,
5 created_at timestamptz DEFAULT now()
6);
7
8-- Enable RLS
9ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;
10
11-- Only admins can manage domains (no public access)
12CREATE POLICY "Only admins can manage allowed domains"
13 ON public.allowed_email_domains
14 FOR ALL
15 TO authenticated
16 USING (
17 (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
18 );
19
20-- Insert your allowed domains
21INSERT INTO public.allowed_email_domains (domain) VALUES
22 ('yourcompany.com'),
23 ('partnerfirm.org');

Expected result: The allowed_email_domains table exists with your approved domains and RLS policies protecting it.

2

Create the domain validation trigger function

Create a trigger function that extracts the domain from the new user's email and checks it against the allowed_email_domains table. The function uses security definer to execute with elevated privileges (since the signup happens before the user is authenticated) and sets search_path to empty string for security. If the domain is not in the allowlist, the function raises an exception, which prevents the row from being inserted into auth.users and returns an error to the client.

typescript
1CREATE OR REPLACE FUNCTION public.check_email_domain()
2RETURNS trigger
3LANGUAGE plpgsql
4SECURITY DEFINER SET search_path = ''
5AS $$
6DECLARE
7 user_domain text;
8BEGIN
9 -- Extract domain from email (lowercase for case-insensitive comparison)
10 user_domain := lower(split_part(NEW.email, '@', 2));
11
12 -- Check if domain is in the allowlist
13 IF NOT EXISTS (
14 SELECT 1 FROM public.allowed_email_domains
15 WHERE domain = user_domain
16 ) THEN
17 RAISE EXCEPTION 'Signups from the domain % are not allowed.', user_domain;
18 END IF;
19
20 RETURN NEW;
21END;
22$$;

Expected result: The trigger function is created and ready to be attached to the auth.users table.

3

Attach the trigger to the auth.users table

Create a BEFORE INSERT trigger on auth.users that calls the validation function. The BEFORE trigger fires before the row is inserted, so it can block the signup entirely. This works for all auth methods — email/password, magic link, OAuth, and phone — because all of them insert a row into auth.users. The trigger checks every new user regardless of how they signed up.

typescript
1CREATE TRIGGER check_email_domain_on_signup
2 BEFORE INSERT ON auth.users
3 FOR EACH ROW
4 EXECUTE FUNCTION public.check_email_domain();

Expected result: The trigger is active and will check every new signup against the allowed domains list.

4

Add client-side domain validation for better UX

While the database trigger provides server-side enforcement, adding client-side validation gives users immediate feedback without waiting for the API call. Check the email domain before calling supabase.auth.signUp() and show a clear error message if the domain is not allowed. This also reduces unnecessary API calls from unauthorized domains.

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8const ALLOWED_DOMAINS = ['yourcompany.com', 'partnerfirm.org']
9
10function isAllowedDomain(email: string): boolean {
11 const domain = email.split('@')[1]?.toLowerCase()
12 return ALLOWED_DOMAINS.includes(domain)
13}
14
15async function signUp(email: string, password: string) {
16 // Client-side check for instant feedback
17 if (!isAllowedDomain(email)) {
18 return { error: 'Only yourcompany.com and partnerfirm.org emails are allowed.' }
19 }
20
21 // Server-side trigger provides the real enforcement
22 const { data, error } = await supabase.auth.signUp({
23 email,
24 password,
25 })
26
27 if (error) {
28 return { error: error.message }
29 }
30
31 return { data }
32}

Expected result: The signup form shows immediate feedback for unauthorized domains and falls back to server-side enforcement.

5

Test the domain restriction

Test that the restriction works by attempting to sign up with both an allowed and a disallowed email domain. The allowed domain should succeed and create a new user. The disallowed domain should fail with the error message you defined in the trigger function. Check the auth.users table in the Dashboard to confirm that only users with allowed domains were created.

typescript
1// Test with allowed domain — should succeed
2const { data, error } = await supabase.auth.signUp({
3 email: 'user@yourcompany.com',
4 password: 'secure-password-123',
5})
6console.log('Allowed domain:', data, error)
7// Expected: data.user exists, error is null
8
9// Test with disallowed domain — should fail
10const { data: data2, error: error2 } = await supabase.auth.signUp({
11 email: 'user@gmail.com',
12 password: 'secure-password-123',
13})
14console.log('Disallowed domain:', data2, error2)
15// Expected: error.message contains 'Signups from the domain gmail.com are not allowed.'

Expected result: Signups from allowed domains succeed, and signups from disallowed domains are blocked with a clear error message.

Complete working example

email-domain-restriction.sql
1-- =============================================
2-- Email Domain Restriction for Supabase Auth
3-- =============================================
4
5-- Step 1: Create the allowed domains table
6CREATE TABLE IF NOT EXISTS public.allowed_email_domains (
7 id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
8 domain text NOT NULL UNIQUE,
9 created_at timestamptz DEFAULT now()
10);
11
12-- Step 2: Enable RLS on the domains table
13ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;
14
15-- Step 3: Only admins can manage allowed domains
16CREATE POLICY "Only admins can manage allowed domains"
17 ON public.allowed_email_domains
18 FOR ALL
19 TO authenticated
20 USING (
21 (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
22 );
23
24-- Step 4: Insert your allowed domains
25INSERT INTO public.allowed_email_domains (domain) VALUES
26 ('yourcompany.com'),
27 ('partnerfirm.org')
28ON CONFLICT (domain) DO NOTHING;
29
30-- Step 5: Create the validation trigger function
31CREATE OR REPLACE FUNCTION public.check_email_domain()
32RETURNS trigger
33LANGUAGE plpgsql
34SECURITY DEFINER SET search_path = ''
35AS $$
36DECLARE
37 user_domain text;
38BEGIN
39 -- Extract and normalize the email domain
40 user_domain := lower(split_part(NEW.email, '@', 2));
41
42 -- Check against the allowlist
43 IF NOT EXISTS (
44 SELECT 1 FROM public.allowed_email_domains
45 WHERE domain = user_domain
46 ) THEN
47 RAISE EXCEPTION 'Signups from the domain % are not allowed. Contact your administrator for access.', user_domain;
48 END IF;
49
50 RETURN NEW;
51END;
52$$;
53
54-- Step 6: Attach the trigger to auth.users
55CREATE TRIGGER check_email_domain_on_signup
56 BEFORE INSERT ON auth.users
57 FOR EACH ROW
58 EXECUTE FUNCTION public.check_email_domain();

Common mistakes when allowing Sign In Only with Specific Domains in Supabase

Why it's a problem: Using security invoker instead of security definer for the trigger function, causing permission errors

How to avoid: The trigger fires during signup when no user is authenticated yet. Use SECURITY DEFINER so the function executes with the privileges of the function owner, who has access to the allowed_email_domains table.

Why it's a problem: Hardcoding allowed domains in the trigger function instead of using a table

How to avoid: Store domains in the allowed_email_domains table. This lets you add or remove domains via SQL or an admin UI without modifying the trigger function.

Why it's a problem: Not lowercasing the email domain before comparison, allowing bypass with mixed-case emails

How to avoid: Use lower(split_part(NEW.email, '@', 2)) to normalize the domain to lowercase before checking against the allowlist.

Why it's a problem: Only implementing client-side domain validation without the database trigger, making it bypassable

How to avoid: The database trigger is the real enforcement. Client-side validation is for UX only. Always implement the trigger even if you have client-side checks.

Best practices

  • Use a database trigger for server-side enforcement and client-side validation for UX
  • Store allowed domains in a table with RLS, not hardcoded in the trigger function
  • Normalize email domains to lowercase before comparison to prevent bypass
  • Use SECURITY DEFINER with SET search_path = '' for trigger functions
  • Write clear error messages in RAISE EXCEPTION that can be displayed in your UI
  • Create an admin UI for managing allowed domains so non-technical team members can update the list
  • Test with both allowed and disallowed domains after setup to confirm the trigger works

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to restrict Supabase signups to specific email domains like @mycompany.com and @partner.org. Show me how to create a PostgreSQL trigger on auth.users that checks the email domain against an allowlist table, and how to add client-side validation in my React signup form.

Supabase Prompt

Create a SQL migration that sets up email domain restriction for Supabase Auth. Include an allowed_email_domains table with RLS, a BEFORE INSERT trigger function on auth.users that checks the domain, and seed data for initial allowed domains.

Frequently asked questions

Does the domain restriction work with OAuth login like Google?

Yes. The trigger fires on every insert into auth.users, regardless of the auth method. When a user signs in with Google, Supabase creates a row in auth.users with their Google email, and the trigger checks the domain. If the domain is not allowed, the OAuth signup is blocked.

Can I add a new allowed domain without modifying the trigger?

Yes. Since the trigger reads from the allowed_email_domains table, you just insert a new row: INSERT INTO allowed_email_domains (domain) VALUES ('newdomain.com'). No function or trigger changes needed.

What error does the user see when their domain is blocked?

The user sees the message from the RAISE EXCEPTION statement in the error.message field of the signUp response. Customize this message in the trigger function to be user-friendly.

Can I block specific domains instead of allowing specific ones?

Yes. Reverse the logic in the trigger: check IF EXISTS instead of IF NOT EXISTS, and name the table blocked_email_domains. This blocks signups from listed domains while allowing all others.

Does this work with magic link login?

Yes. signInWithOtp creates a new user if one does not exist, which triggers the BEFORE INSERT on auth.users. If the domain is not allowed, the signup is blocked. If the user already exists (from a previous allowed signup), magic link login works normally.

How do I temporarily disable the domain restriction?

Drop the trigger with DROP TRIGGER check_email_domain_on_signup ON auth.users. Recreate it later when you want to re-enable the restriction. The trigger function and domains table remain intact.

Can RapidDev help implement domain-based access control for my Supabase project?

Yes. RapidDev can set up domain restrictions, build admin UIs for managing allowed domains, implement role-based access on top of domain restrictions, and configure all related RLS policies.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.