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
Disable public signups in the Dashboard
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.
Create a Supabase admin client with the service role key
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.
1// Server-side only! (e.g., Edge Function, API route)2import { createClient } from '@supabase/supabase-js'34const 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.
Invite a user by email
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.
1// Invite a single user2const { 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)1213if (error) {14 console.error('Invite failed:', error.message)15} else {16 console.log('Invitation sent to:', data.user.email)17}1819// Invite multiple users20const emails = ['user1@company.com', 'user2@company.com', 'user3@company.com']2122for (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.
Build an invite endpoint with a Supabase Edge Function
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.
1// supabase/functions/invite-user/index.ts2import { createClient } from 'npm:@supabase/supabase-js@2'3import { corsHeaders } from '../_shared/cors.ts'45Deno.serve(async (req) => {6 if (req.method === 'OPTIONS') {7 return new Response('ok', { headers: corsHeaders })8 }910 // Verify the caller is authenticated11 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 }1819 // Create admin client20 const supabaseAdmin = createClient(21 Deno.env.get('SUPABASE_URL')!,22 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!23 )2425 // Verify caller is admin26 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 }3839 const { email } = await req.json()40 const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(email)4142 if (error) {43 return new Response(44 JSON.stringify({ error: error.message }),45 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }46 )47 }4849 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.
Customize the invite email template
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.
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
1// Supabase Edge Function: Invite User2// Sends an invitation email to a new user3// Only accessible by authenticated admin users45import { createClient } from 'npm:@supabase/supabase-js@2'67const 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}1213Deno.serve(async (req) => {14 if (req.method === 'OPTIONS') {15 return new Response('ok', { headers: corsHeaders })16 }1718 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 }2627 // Verify the caller is an admin28 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()3435 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 }4142 // Parse the request body43 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 }5051 // Send the invitation using the admin client52 const supabaseAdmin = createClient(53 Deno.env.get('SUPABASE_URL')!,54 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!55 )5657 const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(58 email,59 { data: metadata || {} }60 )6162 if (error) {63 return new Response(64 JSON.stringify({ error: error.message }),65 { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }66 )67 }6869 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation