How to stop anyone from signing up to your internal app
To stop anyone from signing up, turn off public sign-ups in your auth service. Do not rely on a hidden button. The sign-up API stays open until you close it, and anyone who has your public key can call it. A new account then gets the same database role as your team.
This guide uses Supabase for the steps and the code. The same rules apply to other auth services. It shows how to test the problem, close it, remove unknown users, and keep it closed.
Why can anyone sign up to my internal app?
Most auth services create a public sign-up endpoint by default. The endpoint belongs to the auth service. It does not belong to your page. When you remove the sign-up form from your page, the endpoint still exists.
Your front end contains the address of the auth service and the public key. Every visitor can read both. A visitor can send a sign-up request without your page.
A new account is a real user. In Supabase, a signed-in user has the authenticated role. Many AI-written policies say to authenticated using (true). This means any signed-in user can read the table. So the sign-up step decides who can see your data.
The Supabase general configuration documentation names the control. The setting is Allow new users to sign up. When it is on, users can sign up. When you turn it off, only existing users can sign in.
Other ways to create a user exist, and they have their own controls:
| Path | What creates the user | Control |
|---|---|---|
| Email and password | The sign-up endpoint | Allow new users to sign up |
| Magic link or OTP | The first sign-in request | The same setting |
| OAuth (Google, GitHub) | The first OAuth sign-in | The same setting, plus provider rules |
| Anonymous sign-in | A request without credentials | The anonymous sign-ins provider setting |
How do I confirm that anyone can sign up?
Send a sign-up request yourself. Use an email address that you own. Do not use a real team address.
- Find the project URL and the public key. Open your live site, then open DevTools and the Network tab. Look at any request to the auth service. The key is in the
apikeyheader. - Send a sign-up request from a terminal:
curl -i -X POST "https://YOUR-PROJECT.supabase.co/auth/v1/signup" \
-H "apikey: YOUR_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"test+signup@example.com","password":"a-long-random-test-password-1"}'
- Read the response. A success response with a user object means that sign-ups are open. An error response means that the auth service refused the request.
- Open the Authentication page in the dashboard and check the Users list. If the test email is there, sign-ups are open.
- Delete the test user.
Test the other paths too. Request a magic link for a new email address. Try the OAuth button with an account that is not on your team. Test anonymous sign-in with the client library, if your app uses it.
If the auth service sends a confirmation email, the user can stay unconfirmed. Do not treat that as a safe state. Your app code may let unconfirmed users in, and the email setting can change.
How do I stop sign-ups now?
Do these steps in this order. Steps 1 and 2 close the gap. Steps 3 to 6 remove the damage and add a second layer.
Caution: Before you turn off sign-ups, add every team member who needs access. A new person cannot join after you disable sign-ups, unless you add them by another path.
- Turn off sign-ups. In the Supabase dashboard, open Authentication, then the sign-in and provider settings. Turn off Allow new users to sign up. Save the change. For a local project, the same control is
auth.enable_signupin the CLI configuration. The documentation says that this setting allows or disallows new user sign-ups to your project. - Repeat the curl test. The sign-up request must now fail. Test magic link and OAuth again.
- Turn off anonymous sign-ins. The Supabase anonymous sign-ins documentation says that anonymous users use the
authenticatedrole. A policy that allowsauthenticatedthen also allows anonymous users. Check the provider list in the dashboard and disable this provider if you do not need it. - Remove unknown users. See the next section.
- Remove providers that you do not use. Each enabled provider is another way to create a session. Turn off the OAuth providers that your team does not use.
- Add a second layer. See the sections on invites and domain checks below.
Disabling sign-ups does not remove the users who already exist. It also does not end their sessions. Treat that as a separate job.
How do I check the damage?
Find the accounts that came in through the open gap. Read the user list and the logs.
- List all users. Open Authentication, then Users. Sort by the creation date. Compare each account with your team list.
- Check the provider column. An account that uses a provider that your team does not use is a sign of misuse.
- Check the email domains. Look for domains that are not your company. Look for temporary email domains, and for many accounts that have the same pattern, such as
name1@,name2@. - Read the auth logs. In the dashboard, open the logs for Auth. Look for sign-up events and the first sign-in of each unknown account.
- Read the API logs. Look for requests by unknown users to your tables, your storage buckets, and your functions.
- Check what the accounts could do. For each table, read the policies. See which policies allow the
authenticatedrole. The Supabase RLS guide shows how to read them.
You can list users with SQL in the SQL editor. The auth.users table holds them:
select id, email, created_at, last_sign_in_at,
raw_app_meta_data->>'provider' as provider
from auth.users
order by created_at desc;
Compare the result with your team list. For each unknown account, do these steps:
- Delete the user in the dashboard.
- Check the rows that the user created. Look for rows that have the
user_idof that account. - Check the files that the user uploaded.
- Read the rows that the user could read. Decide whether the data left your system. If it contained personal data, find out the rules that apply to you.
Warning: Do not delete rows before you copy them for review. You need the evidence to know what the account did.
How do I let only my team in?
Use an invite list. An invite list is a list of people that you add by hand. Nobody else can create an account.
After sign-ups are off, add each team member with one of these paths:
- The dashboard. Open Authentication, then Users, and add or invite a user by email.
- The admin API. Call
auth.admin.inviteUserByEmail()from a server. This call needs theservice_rolekey, so never run it in the browser. See the service role key guide for how to keep that key private. The reference page describes the call.
// server only
import { createClient } from "@supabase/supabase-js";
const admin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
await admin.auth.admin.inviteUserByEmail("new.person@example.com");
The reference page does not say how this call behaves when sign-ups are off. Test it on a staging project before you depend on it. If it fails, create the user in the dashboard.
Keep a list of the team in your own table, such as team_members. Use it in policies. A policy then names the people who can read a table, and not every signed-in user:
create table public.team_members (
user_id uuid primary key references auth.users (id) on delete cascade
);
alter table public.team_members enable row level security;
create policy "team can read orders"
on public.orders
for select
to authenticated
using (
exists (
select 1 from public.team_members t
where t.user_id = (select auth.uid())
)
);
With this policy, a stray account has no access. This is the second layer. If sign-ups open again by mistake, the data stays closed.
How do I allow only my company email domain?
Use a hook that checks the email at sign-up. The before user created hook runs before the auth service saves a new user. The hook receives the user data, with the email at user.email. It returns {} to allow the sign-up. It returns an error object with a 4xx status code to reject it.
A rejection has this shape:
{ "error": { "http_code": 403, "message": "Sign-ups are limited to company email addresses." } }
The message goes back to the client that tried to sign up. The documentation has a full SQL example and a full Edge Function example for domain rules. Use one of them as a base.
A domain check is weaker than an invite list. Anyone who controls a mailbox at the domain passes it. Companies with open aliases, and with many contractors, often have more such mailboxes than they expect. Use a domain check as the main gate only for low-risk tools.
You can also use the identity provider. For Google sign-in, the Google consent screen has an Internal user type. The Google Cloud help page says that the project can limit authorization requests to members of its Google Cloud organization. Users outside the organization get an error. This limit applies to Google sign-in only. It does not stop email sign-ups, so turn those off too.
What other layers should I add?
Sign-up control is one layer. Add more for an internal tool:
- Row level security on every table. Policies protect data even if an account exists that should not. See the Supabase RLS guide.
- Role checks for sensitive actions. Make a separate admin role in your own table. Do not use a role that a user can change. See privilege escalation in SaaS apps.
- Network limits. If your host supports it, put the tool behind a company login or a VPN. Then people outside the company cannot reach the page at all. This does not replace the auth settings, because the auth service address stays public.
- Short sessions. Set a limit on session length for tools that hold sensitive data.
- A correct view of the public key. The public key is not secret. Its limits come from your policies. See the anon key guide for what it allows.
How do I keep sign-ups closed?
Settings change over time. A person can turn sign-ups on again during a test and forget it. Add a check that finds this.
- Add the curl test to a scheduled job. Run the sign-up request each day. Make the job fail if the response contains a user.
- Use separate projects. Keep the staging and the production projects apart. Then a test setting does not reach production.
- Keep the config in the repository. For Supabase, the local configuration file holds auth settings. Review each change to it.
- Alert on new users. Add a database trigger or a daily query that lists accounts created in the last day. Send the list to your team. An unknown name is then easy to see.
- Review AI-written auth changes. A coding tool can turn on sign-ups to make a test pass. Read the diff for
enable_signup, for provider settings, and for new sign-up forms.
A simple daily query helps:
select email, created_at
from auth.users
where created_at > now() - interval '1 day';
When should I ask for help?
Ask for help in these cases:
- You find accounts that you did not create, and the app holds customer data, payment data, or health data.
- Unknown accounts wrote or deleted rows, and you cannot tell which rows changed.
- You cannot restore the data that unknown accounts changed.
- You must report a possible breach to customers or to a regulator.
Save the user list, the logs, and the policies before you change more. A security professional needs this record to tell what the accounts could reach.
Common questions
Is it enough to remove the sign-up button from the page?
Do OAuth buttons such as Google sign-in also create new users?
Can I allow only people with my company email domain?
Do I still need row level security if only my team has accounts?
How do I add a new team member after I disable sign-ups?
Test your sign up API
A VibeEval scan checks if the sign-up endpoint of your deployed app accepts new accounts and reports the pages that a new account can open.
14-day free trial · No credit card · Cancel anytime