How to check whether Supabase RLS protects your rows
Finding a public key is not the same as finding public data. Start with a clear access rule, inspect the existing configuration, then prove both the allowed and denied cases using invented records in your own test project.
Grants and RLS answer different questions
Table grants determine which operations a database role may attempt. Row Level Security policies determine which rows that operation may affect. A readable exposed table without RLS can disclose data to a role with a SELECT grant. An enabled-RLS badge alone is also insufficient: the policy must match your intended access. See Supabase’s RLS guide.
A publishable key or legacy anon key can belong in client code. A Supabase secret key or legacy service_role key has privileged access and must stay server-side. Test client permissions using ordinary user sessions, not a privileged client. Supabase documents these differences in its API-key guide.
Too broad for private notes
A read policy applies to anonymous visitors and permits every row they can reach.
using (true)Intended private access
An ordinary signed-in user can read rows assigned to that user.
auth.uid() = user_idInspect all the rules that apply
Write down which tables contain private data, which roles need each operation, and whether access belongs to an individual or a tenant. Inspect existing table grants and policies in your project. This read-only query lists policy definitions for the example table name; replace it with a table you own:
select schemaname, tablename, policyname,
permissive, roles, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
and tablename = 'stacksnack_demo_notes';
This policy listing does not show table grants or prove runtime access. Review those separately. PostgreSQL normally combines applicable permissive policies with OR, so adding a narrower policy does not cancel a broader one. Table owners and bypass roles can behave differently from ordinary client roles. The PostgreSQL row-security documentation explains these interactions.
Do not remove a policy just because its name sounds broad. Establish what it permits, whether public access is intentional, and which workflows would change. Review exposed views, storage and server endpoints separately.
Understand a SELECT policy before adapting it
Assume a staging-only table named public.stacksnack_demo_notes already exists with a user_id UUID column matching each note’s owner. This example gives ordinary authenticated users read access to their own rows. It does not create accounts, seed data, or secure your other tables.
-- Illustration for the existing staging-only demo table.
-- Review existing policies before adapting this example.
alter table public.stacksnack_demo_notes
enable row level security;
revoke all on table public.stacksnack_demo_notes
from anon, authenticated;
grant select on table public.stacksnack_demo_notes
to authenticated;
create policy "Demo owners read their own notes"
on public.stacksnack_demo_notes
for select to authenticated
using ((select auth.uid()) = user_id);
This is a read-only client model. The revoke statements remove client write privileges on this demo table. If your app needs writes, design separate INSERT, UPDATE and DELETE grants, policies and tests. Do not apply the snippet unchanged to an existing production table. Existing broad policies must be reviewed; this snippet does not remove them.
For multi-tenant data, ownership by a single user_id is usually not the full access model. Define membership, tenant boundaries and administrative roles explicitly. A signed-in user should not gain another tenant’s records merely by changing a request parameter.
Test allowed and denied access
Seed invented notes for two test accounts in staging. First confirm the fixtures exist through an authorized setup path. Then test as the real client roles; a query in an administrative SQL editor is not a substitute.
| Case | Expected result |
|---|---|
| Signed-out request | Cannot read either private test note. With the demo’s revoked grant, expect a permission error. |
| User A reads A’s note | Returns the known A-only fixture. Proves allowed access still works. |
| User A requests B’s note | Returns no B-only row or contents. |
| User B requests A’s note | Returns no A-only row or contents. |
| Either client attempts a write | Denied in this SELECT-only example. Test your actual write rules separately. |
An empty response by itself proves little: the table might be empty, the filter might be wrong, or you might be using the wrong project. Pair every denial with a known fixture and a working allowed case. Capture whether the result was a permission error or a filtered result, rather than treating every failure as the same thing.
Turn the cases into repeatable database and application tests. Supabase’s testing documentation covers local test workflows. Keep tests with the migration and rerun them after policy changes.
What StackSnack checks—and what it doesn’t
Start with a website scan of public code, known dependency signals and security headers. Website verification unlocks deeper exposed-file checks. Separately approved backend checks can attempt bounded anonymous reads and report masked evidence; they do not persist returned database rows. A URL scan does not log in as users A and B, test every policy operation, or certify tenant isolation.
Read the actual report’s warnings and coverage, even when it has no findings. Continue with the Lovable walkthrough or launch checklist. StackSnack is independent of Supabase.