Guides / Supabase auth

Supabase auth: verify these eight settings before real users arrive.

Row Level Security decides what each user may touch; auth decides who each user is. Both have to hold. AI builders reliably wire the happy-path sign-up and stop there, which leaves a set of defaults and omissions that only show up when a stranger starts probing. These are the eight to check.

The eight checks.

  1. Email confirmation is on. In Authentication settings, confirm that new sign-ups must verify their address before the account works. Without it, anyone can register with any email they like, including your customers' addresses, and your database fills with unverifiable accounts.
  2. The redirect allowlist is tight. Magic links and OAuth flows redirect with tokens attached. Supabase only follows redirects to your Site URL and the additional URLs you explicitly allow; check that list contains only URLs you control, with no wildcards broader than needed. A loose allowlist is a token-theft vector.
  3. Password policy plus leaked-password protection. Set a minimum length in the dashboard, and turn on the built-in check against known-breached passwords. Two toggles, permanent floor under credential quality.
  4. Auth rate limits reviewed. Supabase applies sensible limits to sign-up, sign-in, and OTP endpoints by default; verify you have not raised them for debugging and left them raised. These limits are what stand between you and credential-stuffing scripts.
  5. Session and JWT expiry are sane. The defaults are reasonable; trouble arrives when expiry was stretched to days to paper over a refresh bug. Fix the refresh handling, keep tokens short-lived.
  6. Roles live where users cannot edit them. The single most damaging auth mistake in vibe-coded apps; it gets its own section below.
  7. Admin operations run server-side. Creating users, changing roles, deleting accounts: these belong in edge functions or server code using the service_role key (or a secret key, its replacement on newer Supabase projects), never in browser code trusting a client-sent "isAdmin" flag.
  8. MFA on the accounts that matter. Whatever your app offers users, your own Supabase dashboard login and your deploy accounts get MFA today. The attacker who owns your dashboard owns everything the other seven checks protect.

The metadata trap: how users promote themselves to admin.

Supabase attaches two JSON blobs to every user. app_metadata is written only by privileged server-side calls. user_metadata is editable by the logged-in user through the standard update call in supabase-js. That distinction is the whole game: AI builders routinely store role: "admin" in user_metadata because it is the field the client SDK writes naturally, and any user who opens devtools can then update their own metadata and walk through every role check built on it.

Store roles in app_metadata via a server-side call, or in a table you control, and reference them from RLS:

-- roles in a table the user cannot write
create table public.user_roles (
  user_id uuid primary key references auth.users (id) on delete cascade,
  role text not null default 'member'
);
alter table public.user_roles enable row level security;
-- no insert/update policies for regular users: server-side only

create policy "Admins can read all orders"
on public.orders
for select
to authenticated
using (
  exists (
    select 1 from public.user_roles r
    where r.user_id = (select auth.uid())
      and r.role = 'admin'
  )
);

Then audit for the trap: search your codebase for user_metadata and confirm nothing security-relevant is read from it. The check takes one grep and has paid for itself in every review where we ran it.

Authenticated is a crowd, not a permission Every RLS policy and every rule written as "any authenticated user may..." should be read as "any stranger with a free account may...". Sign-up is public. Authentication establishes identity; authorisation is a separate decision you still have to make.

Questions

Questions founders ask.

What is the difference between user_metadata and app_metadata in Supabase?

user_metadata is editable by the logged-in user through the standard client SDK update call; it is for preferences the user is allowed to change. app_metadata can only be written by privileged server-side calls. Anything security-relevant, roles especially, must live in app_metadata or a protected table, never in user_metadata.

Do I need email confirmation for an early-stage app?

Yes, unless you have a specific reason and understand the trade. Without confirmation, anyone can create accounts with addresses they do not own, which pollutes your data, enables impersonation, and breaks every future email you send. The friction cost is one click per genuine user.

How do I create an admin user safely?

Server-side, in one privileged place: an edge function or script using the service_role key (or a secret key, its replacement on newer Supabase projects) that sets the role in app_metadata or your roles table. Never from the browser, and never based on a value the client sends. The first admin can be set once via SQL directly; every later promotion should go through the same audited path.

Are magic links safe to use?

Yes, with two conditions: the redirect allowlist contains only URLs you control, since the link carries the token to wherever it redirects, and expiry stays short. Magic links move your security boundary to the user's inbox, which for most consumer apps is an acceptable, well-understood trade.

Identity plus access

Auth and RLS only hold together. Get both reviewed at once.

A production-readiness review checks the whole chain: who users are, what they can touch, where the secrets live, and what happens under load, with a ranked fix list at the end.