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

How to Allow Login Only for Invited Users in Supabase

To restrict Supabase to invited users only, disable public signups in the Dashboard under Authentication > Providers > Email, then use supabase.auth.admin.inviteUserByEmail() from a server-side function or Edge Function to send invite emails. Only users who receive an invite email and click the link can create an account. The admin.inviteUserByEmail() method requires the service role key, so it must only be called from server-side code.

What you'll learn

  • How to disable public signups in the Supabase Dashboard
  • How to invite users with supabase.auth.admin.inviteUserByEmail()
  • How to build a server-side invite endpoint using Edge Functions
  • How to customize the invite email template
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

To restrict Supabase to invited users only, disable public signups in the Dashboard under Authentication > Providers > Email, then use supabase.auth.admin.inviteUserByEmail() from a server-side function or Edge Function to send invite emails. Only users who receive an invite email and click the link can create an account. The admin.inviteUserByEmail() method requires the service role key, so it must only be called from server-side code.

Setting Up Invite-Only Authentication in Supabase

By default, anyone can sign up for a Supabase project with Auth enabled. For private applications, internal tools, or beta launches, you want to restrict access to invited users only. This tutorial shows you how to disable public signups, create an invite flow using the admin API, and build a simple admin panel for sending invitations. The invited user receives an email with a magic link that lets them set their password and access the app.

Prerequisites

  • A Supabase project with Auth and email provider enabled
  • Custom SMTP configured (recommended to avoid the 2 emails/hour default limit)
  • A server-side environment for calling the admin API (Edge Function, API route, or server component)
  • @supabase/supabase-js v2+ installed

Step-by-step guide

1

Disable public signups in the Dashboard

Go to the Supabase Dashboard, navigate to Authentication > Providers > Email. Toggle off the 'Allow new users to sign up' option (it may be labeled 'Enable sign ups' depending on your Dashboard version). When this is disabled, calling supabase.auth.signUp() from the client returns an error instead of creating a new account. Only the admin API (inviteUserByEmail) can create new users. This is the foundation of the invite-only system.

Expected result: Public signups are disabled. Calling signUp() from the client returns an error indicating signups are not allowed.

2

Create a Supabase admin client with the service role key

The inviteUserByEmail method is on supabase.auth.admin, which requires the service role key. Create a server-side admin client that uses this key. This client bypasses RLS and has full access to the auth system. Never expose the service role key in client-side code — only use it in Edge Functions, API routes, or server components.

typescript
1// Server-side only! (e.g., Edge Function, API route)
2import { createClient } from '@supabase/supabase-js'
3
4const supabaseAdmin = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!,
7 {
8 auth: {
9 autoRefreshToken: false,
10 persistSession: false,
11 },
12 }
13)

Expected result: A server-side Supabase admin client is created with the service role key.

3

Invite a user by email

Call supabase.auth.admin.inviteUserByEmail() with the user's email address. This creates a new user in auth.users with an unconfirmed status and sends them an invitation email. The email contains a magic link that, when clicked, confirms their account and redirects them to your app where they can set a password. You can optionally pass user metadata (like name or role) and a custom redirect URL.

typescript
1// Invite a single user
2const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(
3 'newuser@company.com',
4 {
5 data: {
6 full_name: 'Jane Smith',
7 role: 'member',
8 },
9 redirectTo: 'https://your-app.com/welcome',
10 }
11)
12
13if (error) {
14 console.error('Invite failed:', error.message)
15} else {
16 console.log('Invitation sent to:', data.user.email)
17}
18
19// Invite multiple users
20const emails = ['user1@company.com', 'user2@company.com', 'user3@company.com']
21
22for (const email of emails) {
23 const { error } = await supabaseAdmin.auth.admin.inviteUserByEmail(email)
24 if (error) {
25 console.error(`Failed to invite ${email}:`, error.message)
26 }
27}

Expected result: An invitation email is sent to the specified address, and a new user record is created in auth.users.

4

Build an invite endpoint with a Supabase Edge Function

Create an Edge Function that accepts an email address and sends an invitation. This function should verify that the caller is an authenticated admin before sending the invite. Check the caller's JWT for an admin role or verify against an admin list. This prevents unauthorized users from sending invitations even if they discover the endpoint.

typescript
1// supabase/functions/invite-user/index.ts
2import { createClient } from 'npm:@supabase/supabase-js@2'
3import { corsHeaders } from '../_shared/cors.ts'
4
5Deno.serve(async (req) => {
6 if (req.method === 'OPTIONS') {
7 return new Response('ok', { headers: corsHeaders })
8 }
9
10 // Verify the caller is authenticated
11 const authHeader = req.headers.get('Authorization')
12 if (!authHeader) {
13 return new Response(
14 JSON.stringify({ error: 'Missing authorization header' }),
15 { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
16 )
17 }
18
19 // Create admin client
20 const supabaseAdmin = createClient(
21 Deno.env.get('SUPABASE_URL')!,
22 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
23 )
24
25 // Verify caller is admin
26 const supabaseUser = createClient(
27 Deno.env.get('SUPABASE_URL')!,
28 Deno.env.get('SUPABASE_ANON_KEY')!,
29 { global: { headers: { Authorization: authHeader } } }
30 )
31 const { data: { user } } = await supabaseUser.auth.getUser()
32 if (!user || user.app_metadata?.role !== 'admin') {
33 return new Response(
34 JSON.stringify({ error: 'Admin access required' }),
35 { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
36 )
37 }
38
39 const { email } = await req.json()
40 const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(email)
41
42 if (error) {
43 return new Response(
44 JSON.stringify({ error: error.message }),
45 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
46 )
47 }
48
49 return new Response(
50 JSON.stringify({ message: `Invitation sent to ${email}` }),
51 { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
52 )
53})

Expected result: An Edge Function is deployed that sends invitations to specified email addresses when called by an admin.

5

Customize the invite email template

Go to the Supabase Dashboard, navigate to Authentication > Email Templates. Select the Invite User template. Customize the HTML to match your branding and include relevant information for your invitees. Use the {{ .ConfirmationURL }} template variable for the magic link. You can also include {{ .Email }} and {{ .SiteURL }}. The invite email is the first impression for new users, so make it clear and professional.

typescript
1<!-- Example invite email template -->
2<h2>You have been invited!</h2>
3<p>You have been invited to join our application.</p>
4<p>Click the link below to accept your invitation and set up your account:</p>
5<p><a href="{{ .ConfirmationURL }}">Accept Invitation</a></p>
6<p>This link expires in 24 hours.</p>
7<p>If you did not expect this invitation, you can ignore this email.</p>

Expected result: The invite email template is customized and includes the confirmation URL magic link.

Complete working example

supabase/functions/invite-user/index.ts
1// Supabase Edge Function: Invite User
2// Sends an invitation email to a new user
3// Only accessible by authenticated admin users
4
5import { createClient } from 'npm:@supabase/supabase-js@2'
6
7const corsHeaders = {
8 'Access-Control-Allow-Origin': '*',
9 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
10 'Access-Control-Allow-Methods': 'POST, OPTIONS',
11}
12
13Deno.serve(async (req) => {
14 if (req.method === 'OPTIONS') {
15 return new Response('ok', { headers: corsHeaders })
16 }
17
18 try {
19 const authHeader = req.headers.get('Authorization')
20 if (!authHeader) {
21 return new Response(
22 JSON.stringify({ error: 'Authorization required' }),
23 { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
24 )
25 }
26
27 // Verify the caller is an admin
28 const supabaseUser = createClient(
29 Deno.env.get('SUPABASE_URL')!,
30 Deno.env.get('SUPABASE_ANON_KEY')!,
31 { global: { headers: { Authorization: authHeader } } }
32 )
33 const { data: { user } } = await supabaseUser.auth.getUser()
34
35 if (!user || user.app_metadata?.role !== 'admin') {
36 return new Response(
37 JSON.stringify({ error: 'Admin privileges required' }),
38 { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
39 )
40 }
41
42 // Parse the request body
43 const { email, metadata } = await req.json()
44 if (!email) {
45 return new Response(
46 JSON.stringify({ error: 'Email is required' }),
47 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
48 )
49 }
50
51 // Send the invitation using the admin client
52 const supabaseAdmin = createClient(
53 Deno.env.get('SUPABASE_URL')!,
54 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
55 )
56
57 const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(
58 email,
59 { data: metadata || {} }
60 )
61
62 if (error) {
63 return new Response(
64 JSON.stringify({ error: error.message }),
65 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
66 )
67 }
68
69 return new Response(
70 JSON.stringify({ message: `Invitation sent to ${email}`, user: data.user }),
71 { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
72 )
73 } catch (err) {
74 return new Response(
75 JSON.stringify({ error: 'Internal server error' }),
76 { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
77 )
78 }
79})

Common mistakes when allowing Login Only for Invited Users in Supabase

Why it's a problem: Calling inviteUserByEmail with the anon key instead of the service role key

How to avoid: The admin API requires the service role key. Create a separate admin client with SUPABASE_SERVICE_ROLE_KEY for invite operations.

Why it's a problem: Forgetting to disable signups for OAuth providers after disabling email signups

How to avoid: Check each OAuth provider in Authentication > Providers and disable signups there too. Otherwise users can create accounts by signing in with Google/GitHub.

Why it's a problem: Hitting the 2 emails/hour SMTP rate limit when sending multiple invitations

How to avoid: Configure a custom SMTP provider in Dashboard > Authentication > SMTP Settings. Services like Resend or SendGrid support hundreds of emails per hour.

Best practices

  • Always disable signups for all auth providers (email and OAuth) when implementing invite-only access
  • Use the service role key only in server-side code and never expose it to the client
  • Verify the caller is an admin before processing invite requests in your Edge Function
  • Configure custom SMTP before sending invitations to avoid the 2 emails/hour default limit
  • Customize the invite email template to match your app's branding and set clear expectations
  • Store the invite metadata (who invited whom, when) in a custom table for audit purposes
  • Add CORS headers to your Edge Function so it can be called from your frontend

Still stuck?

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

ChatGPT Prompt

I want to make my Supabase app invite-only. Show me how to disable public signups, create a Supabase Edge Function that sends invitations using supabase.auth.admin.inviteUserByEmail(), and verify the caller is an admin before sending the invite.

Supabase Prompt

Create a Supabase Edge Function called invite-user that accepts a POST request with an email address, verifies the caller has an admin role in their JWT, and sends an invitation using the admin API. Include CORS headers and proper error handling.

Frequently asked questions

Can invited users still use email/password login after accepting the invitation?

Yes. When a user clicks the invite link, they are prompted to set a password. After that, they can sign in with their email and password normally. The invite just creates the initial account.

What happens if I invite someone who already has an account?

The inviteUserByEmail method returns an error if the email is already registered. Check for this error and handle it in your invite flow — you might want to show a message like 'This user already has an account.'

How long does the invite link stay valid?

The default invite link expiration is 24 hours. You can configure this in Dashboard > Authentication > Auth Settings. After expiration, you need to resend the invitation.

Can I invite users in bulk?

Yes. Loop through an array of email addresses and call inviteUserByEmail for each one. Add a small delay between calls if you have a large list to avoid rate limits.

Does disabling signups affect existing users?

No. Disabling signups only prevents new accounts from being created via signUp(). Existing users can still sign in, and the admin API can still invite new users.

Can I use inviteUserByEmail from the Supabase Dashboard?

Yes. Go to Authentication > Users and click 'Invite' or 'Add user' to send an invitation directly from the Dashboard without writing any code.

Can RapidDev help build a complete invite-only authentication system?

Yes. RapidDev can build the full invite flow including an admin panel for sending invitations, custom email templates, role assignment during invite, and onboarding flows for new users accepting invitations.

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.