# How to Allow Sign In Only with Specific Domains in Supabase

- Tool: Supabase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+, PostgreSQL 15+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
-- Create the allowed domains table
CREATE TABLE public.allowed_email_domains (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  domain text NOT NULL UNIQUE,
  created_at timestamptz DEFAULT now()
);

-- Enable RLS
ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;

-- Only admins can manage domains (no public access)
CREATE POLICY "Only admins can manage allowed domains"
  ON public.allowed_email_domains
  FOR ALL
  TO authenticated
  USING (
    (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
  );

-- Insert your allowed domains
INSERT INTO public.allowed_email_domains (domain) VALUES
  ('yourcompany.com'),
  ('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.

```
CREATE OR REPLACE FUNCTION public.check_email_domain()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = ''
AS $$
DECLARE
  user_domain text;
BEGIN
  -- Extract domain from email (lowercase for case-insensitive comparison)
  user_domain := lower(split_part(NEW.email, '@', 2));

  -- Check if domain is in the allowlist
  IF NOT EXISTS (
    SELECT 1 FROM public.allowed_email_domains
    WHERE domain = user_domain
  ) THEN
    RAISE EXCEPTION 'Signups from the domain % are not allowed.', user_domain;
  END IF;

  RETURN NEW;
END;
$$;
```

**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.

```
CREATE TRIGGER check_email_domain_on_signup
  BEFORE INSERT ON auth.users
  FOR EACH ROW
  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.

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

const ALLOWED_DOMAINS = ['yourcompany.com', 'partnerfirm.org']

function isAllowedDomain(email: string): boolean {
  const domain = email.split('@')[1]?.toLowerCase()
  return ALLOWED_DOMAINS.includes(domain)
}

async function signUp(email: string, password: string) {
  // Client-side check for instant feedback
  if (!isAllowedDomain(email)) {
    return { error: 'Only yourcompany.com and partnerfirm.org emails are allowed.' }
  }

  // Server-side trigger provides the real enforcement
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  })

  if (error) {
    return { error: error.message }
  }

  return { data }
}
```

**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.

```
// Test with allowed domain — should succeed
const { data, error } = await supabase.auth.signUp({
  email: 'user@yourcompany.com',
  password: 'secure-password-123',
})
console.log('Allowed domain:', data, error)
// Expected: data.user exists, error is null

// Test with disallowed domain — should fail
const { data: data2, error: error2 } = await supabase.auth.signUp({
  email: 'user@gmail.com',
  password: 'secure-password-123',
})
console.log('Disallowed domain:', data2, error2)
// 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 code example

File: `email-domain-restriction.sql`

```sql
-- =============================================
-- Email Domain Restriction for Supabase Auth
-- =============================================

-- Step 1: Create the allowed domains table
CREATE TABLE IF NOT EXISTS public.allowed_email_domains (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  domain text NOT NULL UNIQUE,
  created_at timestamptz DEFAULT now()
);

-- Step 2: Enable RLS on the domains table
ALTER TABLE public.allowed_email_domains ENABLE ROW LEVEL SECURITY;

-- Step 3: Only admins can manage allowed domains
CREATE POLICY "Only admins can manage allowed domains"
  ON public.allowed_email_domains
  FOR ALL
  TO authenticated
  USING (
    (SELECT auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
  );

-- Step 4: Insert your allowed domains
INSERT INTO public.allowed_email_domains (domain) VALUES
  ('yourcompany.com'),
  ('partnerfirm.org')
ON CONFLICT (domain) DO NOTHING;

-- Step 5: Create the validation trigger function
CREATE OR REPLACE FUNCTION public.check_email_domain()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = ''
AS $$
DECLARE
  user_domain text;
BEGIN
  -- Extract and normalize the email domain
  user_domain := lower(split_part(NEW.email, '@', 2));

  -- Check against the allowlist
  IF NOT EXISTS (
    SELECT 1 FROM public.allowed_email_domains
    WHERE domain = user_domain
  ) THEN
    RAISE EXCEPTION 'Signups from the domain % are not allowed. Contact your administrator for access.', user_domain;
  END IF;

  RETURN NEW;
END;
$$;

-- Step 6: Attach the trigger to auth.users
CREATE TRIGGER check_email_domain_on_signup
  BEFORE INSERT ON auth.users
  FOR EACH ROW
  EXECUTE FUNCTION public.check_email_domain();
```

## Common mistakes

- **Using security invoker instead of security definer for the trigger function, causing permission errors** — undefined Fix: 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.
- **Hardcoding allowed domains in the trigger function instead of using a table** — undefined Fix: 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.
- **Not lowercasing the email domain before comparison, allowing bypass with mixed-case emails** — undefined Fix: Use lower(split_part(NEW.email, '@', 2)) to normalize the domain to lowercase before checking against the allowlist.
- **Only implementing client-side domain validation without the database trigger, making it bypassable** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-allow-sign-in-only-with-specific-domains-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-allow-sign-in-only-with-specific-domains-in-supabase
