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
Create the allowed domains table
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.
1-- Create the allowed domains table2CREATE 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);78-- Enable RLS9ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;1011-- Only admins can manage domains (no public access)12CREATE POLICY "Only admins can manage allowed domains"13 ON public.allowed_email_domains14 FOR ALL15 TO authenticated16 USING (17 (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'18 );1920-- Insert your allowed domains21INSERT INTO public.allowed_email_domains (domain) VALUES22 ('yourcompany.com'),23 ('partnerfirm.org');Expected result: The allowed_email_domains table exists with your approved domains and RLS policies protecting it.
Create the domain validation trigger function
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.
1CREATE OR REPLACE FUNCTION public.check_email_domain()2RETURNS trigger3LANGUAGE plpgsql4SECURITY DEFINER SET search_path = ''5AS $$6DECLARE7 user_domain text;8BEGIN9 -- Extract domain from email (lowercase for case-insensitive comparison)10 user_domain := lower(split_part(NEW.email, '@', 2));1112 -- Check if domain is in the allowlist13 IF NOT EXISTS (14 SELECT 1 FROM public.allowed_email_domains15 WHERE domain = user_domain16 ) THEN17 RAISE EXCEPTION 'Signups from the domain % are not allowed.', user_domain;18 END IF;1920 RETURN NEW;21END;22$$;Expected result: The trigger function is created and ready to be attached to the auth.users table.
Attach the trigger to the auth.users table
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.
1CREATE TRIGGER check_email_domain_on_signup2 BEFORE INSERT ON auth.users3 FOR EACH ROW4 EXECUTE FUNCTION public.check_email_domain();Expected result: The trigger is active and will check every new signup against the allowed domains list.
Add client-side domain validation for better UX
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.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78const ALLOWED_DOMAINS = ['yourcompany.com', 'partnerfirm.org']910function isAllowedDomain(email: string): boolean {11 const domain = email.split('@')[1]?.toLowerCase()12 return ALLOWED_DOMAINS.includes(domain)13}1415async function signUp(email: string, password: string) {16 // Client-side check for instant feedback17 if (!isAllowedDomain(email)) {18 return { error: 'Only yourcompany.com and partnerfirm.org emails are allowed.' }19 }2021 // Server-side trigger provides the real enforcement22 const { data, error } = await supabase.auth.signUp({23 email,24 password,25 })2627 if (error) {28 return { error: error.message }29 }3031 return { data }32}Expected result: The signup form shows immediate feedback for unauthorized domains and falls back to server-side enforcement.
Test the domain restriction
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.
1// Test with allowed domain — should succeed2const { 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 null89// Test with disallowed domain — should fail10const { 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
1-- =============================================2-- Email Domain Restriction for Supabase Auth3-- =============================================45-- Step 1: Create the allowed domains table6CREATE 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);1112-- Step 2: Enable RLS on the domains table13ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;1415-- Step 3: Only admins can manage allowed domains16CREATE POLICY "Only admins can manage allowed domains"17 ON public.allowed_email_domains18 FOR ALL19 TO authenticated20 USING (21 (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'22 );2324-- Step 4: Insert your allowed domains25INSERT INTO public.allowed_email_domains (domain) VALUES26 ('yourcompany.com'),27 ('partnerfirm.org')28ON CONFLICT (domain) DO NOTHING;2930-- Step 5: Create the validation trigger function31CREATE OR REPLACE FUNCTION public.check_email_domain()32RETURNS trigger33LANGUAGE plpgsql34SECURITY DEFINER SET search_path = ''35AS $$36DECLARE37 user_domain text;38BEGIN39 -- Extract and normalize the email domain40 user_domain := lower(split_part(NEW.email, '@', 2));4142 -- Check against the allowlist43 IF NOT EXISTS (44 SELECT 1 FROM public.allowed_email_domains45 WHERE domain = user_domain46 ) THEN47 RAISE EXCEPTION 'Signups from the domain % are not allowed. Contact your administrator for access.', user_domain;48 END IF;4950 RETURN NEW;51END;52$$;5354-- Step 6: Attach the trigger to auth.users55CREATE TRIGGER check_email_domain_on_signup56 BEFORE INSERT ON auth.users57 FOR EACH ROW58 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation