Supabase makes it very easy to build a real app quickly.
That speed is why vibe coders love it.
It is also why database access rules get skipped.
If your app stores user data, account data, payments, private content, uploaded files, API usage, chat history or team records, you need to understand Row Level Security.
What RLS does
Row Level Security controls which rows a user can read, insert, update or delete.
Without it, a query might return more data than the current user should see.
In a multi-user app, that can mean:
- one user sees another user's records
- one team accesses another team's workspace
- public visitors can read private rows
- authenticated users can update records they do not own
- deleted or draft content leaks through broad selects
RLS is not a nice-to-have. It is a data boundary.
The dangerous default
The risky pattern looks like this:
Frontend uses Supabase client -> queries table directly -> table has weak or missing RLS
If the frontend can query a table directly, the table policies must enforce access correctly.
Do not rely on "the UI only asks for the current user's rows."
Users can change client-side requests.
Ask for a table inventory
Start by listing every table and what access it needs.
Use this prompt:
Review this Supabase database as a security reviewer.
Create a table-by-table access matrix.
For each table, say:
- what data it stores
- whether it contains personal, private, payment, or tenant-scoped data
- who should be able to select rows
- who should be able to insert rows
- who should be able to update rows
- who should be able to delete rows
- whether RLS should be enabled
- what policies are needed
This forces the conversation away from "does the query work?" and toward "who should be allowed to see this?"
Owner-only access
The common policy shape is owner-only access.
In plain English:
A user can read and update rows where
user_idequals their authenticated user ID.
The exact SQL depends on your schema, but the idea is stable.
Ask:
Write owner-only RLS policies for tables that contain user-owned data.
Use the existing user ID column names from the schema.
Include select, insert, update and delete policies separately.
Explain what each policy allows and what it blocks.
Do not accept generated policies blindly. Read them.
Check that they reference the correct user column and do not accidentally allow all authenticated users.
Team or tenant access
Team apps are more complex.
Rows may belong to an organisation, workspace, project or account. Users may have roles inside that tenant.
The policy needs to answer:
- is the current user a member of this tenant?
- what role do they have?
- can that role perform this action?
- what happens when membership is revoked?
Use this prompt:
Design RLS policies for a multi-tenant app.
Use membership tables rather than trusting client-supplied organisation IDs.
Separate owner, admin, member and read-only access.
Explain how each policy prevents one tenant from reading or modifying another tenant's rows.
Service role keys
Supabase service role keys bypass RLS.
That is useful on trusted servers.
It is catastrophic in the browser.
Search:
rg "service_role|SUPABASE_SERVICE_ROLE|anon"
Ask:
Find every Supabase key usage in this app.
Confirm that anon keys only appear where safe, and service role keys are only used server-side in trusted code paths.
Flag any key that could be bundled into the browser or exposed through logs.
Watch for Supabase-specific traps
There are a few Supabase details that are easy to miss:
- Do not use user-editable metadata for authorization decisions.
- Views can bypass RLS unless configured carefully.
- Updating rows usually also needs a matching select policy.
- Storage upserts may need insert, select and update permissions.
- Security definer functions should not live in an exposed schema.
Ask:
Review this Supabase setup for common security traps.
Check:
- whether authorization depends on user-editable metadata
- whether views should use security_invoker or be moved out of exposed schemas
- whether update policies also have the select access they need
- whether storage policies support the intended upload and replace behaviour
- whether security definer functions are exposed to anon or authenticated roles
These details are not theoretical. They are the kind of configuration mistakes that make an app look fine in the UI while the database boundary is weaker than expected.
Server-side queries still need discipline
Moving database calls to the server helps, but it does not remove the need for authorization.
Server routes must still check:
- who is making the request
- what account or tenant they belong to
- whether they can access the requested record
- whether the response returns only required fields
Ask:
Review server-side Supabase queries.
Find any route that:
- trusts an ID from the client without checking ownership
- returns entire rows when the UI only needs a few fields
- uses service role access unnecessarily
- does not handle missing or unauthorized records distinctly
Verify with negative tests
Do not only test your own account.
Create two test users:
- User A owns record A
- User B owns record B
Then try:
- User A reading record B
- User A updating record B
- logged-out user reading private rows
- normal member performing admin action
The expected result is denial.
If you never test the denied path, you do not know whether the boundary exists.
What PageLens can help with
PageLens can inspect public and authenticated website surfaces, and authenticated scans help reveal logged-in UX, trust and security signals.
RLS itself is a database-level control, so you still need schema and policy review.
Use PageLens to find what is visible from the app surface. Use Supabase policy review to prove users cannot cross data boundaries underneath it.