1.Two different problems
| Open to anyone | Open to every signed-in user | |
|---|---|---|
| Who can read the rows | Any stranger. No sign-in needed. | Anyone with an account. If anyone can sign up, that means anyone. |
| Usual cause on Supabase | Row level security is off for the table, or a read rule lets signed-out visitors through. | Row level security is on, but the read rule lets every signed-in user through, not only the owner. |
Open to anyone. On Supabase, every visitor’s browser talks to the database directly with a public key (why that key is fine), so the database rules are the only lock. Supabase’s row level security docs warn that a table without row level security can be read and changed by any role that has access to it. On projects that still grant that access by default, signed-out visitors have it on every new table.
Open to every signed-in user. A stranger sees nothing, so the app looks safe. But the read rule says “any signed-in user” instead of “the owner”, so every customer reads every customer’s rows. If anyone can sign up, even without confirming an email (why that matters), “signed-in users only” protects very little. Supabase’s docs also note that anonymous sign-ins count as signed-in users to the database.
2.How row level security works, in plain words
Row level security (RLS) is a feature of Postgres, the database inside Supabase. A rule, which Supabase calls a policy, belongs to one table. The database checks it on every request, as if it added a filter like “only rows where user_id is me” to each query.
- One rule per action. The actions are read (select), add (insert), change (update) and delete (delete). Supabase’s docs advise a separate rule for each.
- Each private row has an owner. A column such as user_id holds the ID of the user who owns the row. Inside a rule, auth.uid() gives the ID of the user making the request. The rule compares the two.
- Strangers match nothing. With no signed-in user, auth.uid() is empty (null), so an owner rule matches no rows.
- RLS on with no rules means no access. Through the public key, nobody can read or change the table until you add rules. Do not fix that with a “let everyone through” rule.
- Adding and changing also check the new row. A “with check” part makes sure the new or changed row belongs to the user, so nobody can create a row in someone else’s name or hand a row to another user.
The same docs name a few traps. Views (saved queries that look like tables) skip row level security by default; on Postgres 15 and later, create them with security_invoker = true. The secret key (the service_role role) can skip every rule, so it must never reach the browser. Do not build a rule on user_metadata, because users can change their own.
3.Test it with two accounts of your own
Run these tests only on an app you own, with accounts and email addresses of your own.
- List every table that holds private data: orders, bookings, messages, profiles, invoices.
- In your normal browser window, sign up as customer A and create one private record of each kind.
- Open a private browser window and sign up as customer B, with a second email address.
- As B, open every page that lists data, including any dashboard or /admin page. You should see none of A’s records.
- Copy the address of one of A’s record pages and open it as B. You should get an error or an empty page. If the record has an edit page, try to save a change as B, then check as A that nothing changed.
- As B, open your browser’s developer tools, go to the Network tab and reload each page. Read the responses. If A’s name, email or record appears in any of them, the server sent it, even if the page hides it.
- Sign out and repeat the last step as a stranger.
Supabase’s Security Advisor, in the dashboard, flags tables with row level security off and rules that let everyone through. Lovable’s Quick scan runs each time you publish and flags the same kinds of problems; its Deep scan looks for users who can view or change data that is not theirs. Lovable says its scans do not replace a full security review. With the Supabase command line tool, the docs give a test file per table that checks each action for signed-out and signed-in users.
4.The fix on Supabase
This is the pattern from Supabase’s row level security docs, on an example orders table. On plain Supabase, run it in the dashboard’s SQL Editor. On Lovable Cloud, paste it into the chat, ask Lovable to apply it to your table, then review the rules under More → Database → Manage → Policies.
-- Example: an orders table whose user_id column holds the owner's ID.
alter table public.orders enable row level security;
-- Signed-out visitors get nothing. Signed-in users get only their own rows.
revoke all on table public.orders from anon, authenticated;
grant select, insert, update, delete on table public.orders to authenticated;
create policy "Customers read their own orders"
on public.orders for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "Customers add orders only for themselves"
on public.orders for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "Customers change only their own orders"
on public.orders for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "Customers delete only their own orders"
on public.orders for delete
to authenticated
using ( (select auth.uid()) = user_id );- Leave out any action your customers should not do, in both the grant line and the rules.
- Keep the read rule: the docs say an update does not work as expected without a matching select rule.
- The change rule lets customers change any column in their own rows, such as a status or a paid column. Columns that only your server should set need more: see how users get paid features for free.
Some data is meant to be public, like reviews. Only for that kind of data, grant select to anon too and add a read rule “to anon, authenticated using ( true )”. Rules work on rows, not columns, so keep private columns out of a public table: if the reviews are public but the reviewer’s email and phone are not, move those to a separate table with no public rules. Then run the two-account test again.
5.A real result: LidTight’s check on Tidybook
Tidybook is LidTight’s own demo app, built with Lovable on Lovable Cloud (Supabase), with two tables: bookings and reviews. On 1 November 2026, LidTight ran its free check and its deep checks on it. The deep checks signed up two new test users, A and B.
| What LidTight tried | Result on Tidybook |
|---|---|
| A stranger reads the reviews table, with no sign-in | Failed, critical: “Anyone can read your reviews table: 12 rows, with first_name, email, phone”. The rows were counted, not downloaded. A few values were read only to confirm they were real, and not kept. |
| Test user B reads test user A’s booking | Passed: “user B could not see user A’s bookings row”. |
| Test user B changes test user A’s booking | Passed: “the server blocked it”. |
The same app failed the first test and passed the other two. Rules belong to each table: bookings kept A’s row away from B, while reviews was open to any stranger.
Passing does not make a table safe, either. The same deep checks found a second critical problem in bookings: test user A changed its own booking from “unpaid” to “paid”, and no payment was made. An owner rule allows that, because the row is A’s own.
6.What LidTight shows, and what it cannot see
LidTight’s free check reads only what any visitor’s browser can see, and counts the rows a stranger can read in each table: the first problem. The free deep checks run when you tick the box under the app link. They sign up two test users. User B counts the rows that belong to other users, then tries to change a test record of user A; any change is put back. The test users stay, and so does the record if your app does not let users delete it. That covers the second problem.
LidTight tests from the outside, so it shows what your database returns, not why: it does not read your rules. The two-user test works on Supabase and Lovable Cloud apps, not yet on Base44 or Firebase, and only on tables where a column such as user_id or owner_id names the owner. Your own two-account test and Supabase’s Security Advisor cover the rest. For everything to check before launch, see the launch security checklist.