Supabase auth or RLS broken? How to find the cause
5 min read
On this page
Supabase gives an app a database, sign-in and file storage in one place, which is why so many AI-built and early-stage apps use it. When something goes wrong, though, the symptoms are confusing: a user logs in and lands back on the login page, a page that worked yesterday shows nothing, or worse, a test account can see everyone's data. Almost all of these trace back to two areas: auth configuration and Row Level Security (RLS). This page helps you narrow down which one you are facing.
Match the symptom to the likely cause
| What you see | Most likely area |
|---|---|
| Confirmation or reset emails never arrive | Email sending (SMTP) or templates |
| Clicking the email link opens localhost or an error page | Redirect URL settings |
| Logged in, then bounced back to login on refresh | Session handling with server rendering |
| Pages load but lists are empty | RLS policy does not match the user |
| Any user can see or edit other users' records | RLS off, or policies too open |
| Inserts fail with a "violates row-level security policy" error | Missing or wrong with check |
Auth problems
Redirect URLs
In the Supabase dashboard, under Authentication and then URL Configuration, there is a Site URL and a list of Redirect URLs. Supabase only sends users back to addresses on that list. A common situation: the app was built in a preview environment, the Site URL still points at localhost or an old preview address, and every confirmation link sends users somewhere that no longer exists. Add your production domain and its callback path, and add entries for any preview domains you still use.
Email templates and SMTP
The default email sender is fine for trying things out but is rate-limited and not meant for production. If sign-ups work for the first few people and then stop, this is usually why. Setting up custom SMTP with a transactional email provider fixes the sending side. Also check the templates themselves: if your app uses server-side rendering, the template link often needs to point at a route in your app (for example /auth/confirm) that exchanges the token for a session, rather than the default link.
Session handling with SSR
Apps built with Next.js, Remix, SvelteKit or similar frameworks render some pages on the server. The server can only see the session if it is stored in cookies, which is what the @supabase/ssr package handles. Login loops usually mean one of three things: the old browser-only client is being used on the server, the middleware that refreshes the session cookie is missing or does not run on the right paths, or cookies are not being written back in the response.
There is also a security detail. On the server, getSession() reads whatever is in the cookie without checking it with Supabase, so it should not be used to decide what a user may access. Use getUser(), or getClaims() in newer library versions, which validate the token.
RLS problems
RLS is a set of rules attached to each table. When it is enabled, every query from the browser is filtered through those rules using the identity of the logged-in user, available in SQL as auth.uid(). Here are the patterns that cause most trouble.
Policies with using (true)
using (true) means "this rule matches every row for everyone it applies to". AI tools often add it to make an error go away. On a public catalogue it may be fine for reading. On profiles, orders, messages or anything personal, it is a data leak.
Missing with check
using decides which existing rows a user can see, update or delete. with check decides what a new or changed row is allowed to look like. An update policy without with check can let a user change a row's user_id to someone else's, and an insert policy without it can let a user create rows owned by another account.
Service role misuse
The service role key ignores RLS entirely. It belongs only in server code you control. If it has been placed in the frontend to "fix" permission errors, every visitor effectively has full database access. Search your code and built files for it, and if it was ever exposed, rotate it in the project's API settings.
A correct basic pattern
For a table where each user owns their own rows, a solid starting point looks like this:
alter table public.notes enable row level security;
create policy "Owners can read their notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Owners can add notes for themselves"
on public.notes for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Owners can edit their notes"
on public.notes for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Owners can delete their notes"
on public.notes for delete
to authenticated
using ( (select auth.uid()) = user_id );
create index if not exists notes_user_id_idx on public.notes (user_id);
Wrapping auth.uid() in (select ...) lets Postgres evaluate it once per query instead of once per row, and the index keeps the filter fast as the table grows. Shared data, such as team workspaces, needs policies that check membership in another table, which is where a developer's help is most useful.
Two less obvious places to check: storage buckets have their own policies on storage.objects, and database views ignore the caller's RLS by default unless created with security_invoker turned on.
Checks you can do yourself
- In the table editor, confirm RLS is enabled on every table that holds user data.
- Export your policies and run them through the Supabase RLS checker.
- Search your project for
service_roleusing the secret leak scanner. - Create two test users and confirm neither can see the other's records.
- Compare the Site URL and Redirect URLs with your real domains.
- Send yourself a password reset from the live site and follow the link.
What a rescue developer will typically check first
A developer picking this up will usually read the migration files or the schema to see every table and policy in one place, rather than clicking through the dashboard. They will check which Supabase client is used where (browser, server, middleware), test the sign-up, confirm, login, refresh and logout flow on the real domain, and look for any server code using the service role key without its own permission checks. They will often add tests that run queries as different users so the policies stay correct after future changes.
Expect them to ask to be invited to your Supabase organisation as a member. The guide on giving a developer access without sharing passwords covers this, and why AI-built apps break in production explains how these issues usually arise. If you suspect wider problems, see security issues in AI-generated apps.
Post it as a takeover
If the fix is beyond a quick settings change, post your project on TakeoverWork for free. Note the symptoms from the table above, the framework you use, and whether real users are affected. Never include keys or passwords in the listing. TakeoverWork only connects people: it does not check developers or handle payments, so agree the work and price directly with the person you pick.
Live matching takeovers
No matching takeovers are open right now
New projects are posted regularly. Browse every open takeover, or post your own project free.
Browse all takeoversFrequently asked questions
Why does my query return an empty list instead of an error?
When RLS is enabled and no policy allows the current user to see a row, Supabase simply leaves that row out of the result. An empty list usually means the policy does not match, the user is not logged in when the query runs, or the user_id column holds a different value than auth.uid().
Is using (true) ever acceptable in a policy?
It can be, for data that is genuinely public, such as a list of published blog posts, and only on select. On insert, update or delete, or on any table holding personal or paid data, using (true) usually means anyone can change or read everything.
Where should the service role key be used?
Only in server-side code you control, such as an edge function, a backend route or a scheduled job. It bypasses RLS completely, so it must never appear in browser code, in a mobile app bundle or in a public repository.
Why do confirmation emails stop arriving after a few sign-ups?
The built-in Supabase email sender is meant for testing and has a low sending limit. For a live app, set up custom SMTP with an email provider in the Auth settings, and check the sender domain's SPF and DKIM records so messages are not marked as spam.
Login works locally but not on my live domain. What is wrong?
Check the Site URL and Redirect URLs in the Supabase Auth URL configuration. If the live domain or its callback path is not listed, Supabase sends users back to the wrong place or refuses the redirect. Preview deployments with changing URLs need their own entries or a suitable wildcard.
Stuck with a half-built app?
Post your project free. Developers who finish and rescue projects can send you an interest note, and you decide who to talk to. You agree scope and payment directly with them.
Free tools that help
Related guides
Lovable, Bolt and v0 apps: the most common reasons they break in production
AI app builders are excellent for prototypes. These are the gaps that usually appear when a prototype meets real users, real data and a real domain, and how to close them.
ReadFrom vibe-coded prototype to production: the 25-point checklist
Your AI-built prototype works in the demo. These 25 checks, grouped into ten areas, cover what usually needs attention before real users and real money arrive.
ReadHow to give a developer access without sharing passwords
Use team invites and limited roles instead of shared passwords, service by service, and know exactly how to remove access and rotate keys when the work ends.
ReadRelated problems
Security issues in AI-generated apps: what to check and how to fix them
AI app builders ship working features fast, but often leave keys in the browser, tables open and API routes unprotected. Here is how to check yours.
ReadLovable app not working? How to find what broke and get it fixed
Most Lovable apps that break do so for a short list of reasons: auth settings, database policies, keys, deploys, edge functions or payments. Here is how to narrow it down.
ReadHow to finish a half-built app without starting over by accident
A practical path for a stalled app: take stock of what works, decide fix or rebuild with clear reasons, define finished, prepare access and post a clear listing.
Read