Supabase RLS Mistakes AI-Built Apps Commonly Miss
A founder-readable guide to row-level security, tenant isolation, service-role boundaries, storage policies and the tests that matter before launch.
Liam Glynn
Founder-operator and AI-native product engineer
Direct answer
The most common Supabase RLS failure is assuming that authentication automatically protects data. RLS must be enabled on every exposed table, each policy must express the real ownership or tenant rule, privileged functions and service-role credentials must remain server-side, and cross-user tests must prove that unauthorized records cannot be read or changed.
Supabase makes it possible to build a sophisticated application quickly because authentication, Postgres, storage and APIs arrive as an integrated system. That convenience can also hide a critical distinction: a user being signed in does not mean the user is authorized to access a particular row.
Row-level security is the database layer that enforces that distinction. When it is incomplete, the interface may look private while the underlying API still exposes records to another authenticated user—or, in the worst case, to anonymous traffic.
This guide explains the mistakes that matter most in AI-built products. It is not a penetration-testing methodology. It is a practical review of the permission assumptions founders should be able to explain and verify.
Mistake 1: RLS is missing on one exposed table
A product may secure its obvious tables and forget a supporting table introduced later: notifications, attachments, audit records, profile settings or join tables. Because Supabase can expose tables through its generated API, one omission can bypass the privacy implied by the interface.
Inventory every table in exposed schemas. Confirm RLS is enabled, then confirm the relevant roles have no broader grants than intended. Treat new tables as denied by default until their access model is defined.
“RLS is not a project-level switch. It is a table-by-table control that must survive every migration.”
Mistake 2: Policies check the user but ignore the tenant
Many products begin as single-user apps and later add organizations, clients or teams. A policy such as user_id = auth.uid() may no longer represent the business model once records belong to a tenant and users can join or leave that tenant.
Use a membership relationship that includes tenant, user, role and active status. Then scope the protected record to the same tenant. Test what happens when the membership is suspended or deleted.
Appropriate for genuinely personal records owned by one user.
Appropriate for shared organization data with explicit roles.
Appropriate when an external client should see only one project or workspace.
Mistake 3: The frontend filter is doing the security work
Filtering a query by the current user or tenant is good application behavior, but it is not the security boundary. A user can alter a browser request, call the generated API directly or discover another route that omits the filter.
The database policy must make the unsafe query return no rows or raise an authorization failure even when the client sends a broader request.
Mistake 4: Service-role credentials reach the browser
The Supabase service role bypasses RLS. It belongs only in trusted server or Edge Function environments. A variable name beginning with a public prefix, a client-side import or a generated configuration object can expose it unintentionally.
Review the final browser bundle and deployment environment, not only the source file. Rotate the credential immediately if it has ever been shipped to a client environment or public repository.
Mistake 5: Views and security-definer functions create a side door
Database functions can be the right way to perform an atomic privileged workflow. They can also bypass expected policies when SECURITY DEFINER is used without strict validation, a fixed search path and narrow execution grants.
Each privileged function should validate the caller or be callable only from a trusted service boundary. It should accept the minimum identifiers required, derive ownership server-side and fail closed when records do not match.
Set it explicitly to reduce object-resolution surprises.
Revoke broad execution and grant only to the roles that need it.
Validate tenant, user and record relationships inside the function.
Use one transaction for workflows that must not partially complete.
Mistake 6: Storage policies do not match database policies
A project table can be private while its documents remain public. Storage buckets, object paths and signed URLs need the same ownership model as the records they support.
Use server-derived paths where possible. Avoid trusting a client-supplied tenant or user segment. Verify upload, list, read, replace and delete separately because each operation can require a different policy.
Test RLS as the wrong user, not only the right user
A positive test proves that an intended user can access a record. A security review needs negative tests that prove every other user cannot.
Create two users in different tenants and one user with a lower role in the same tenant. Attempt reads and mutations using known record IDs. Repeat the tests through the same browser, API and function paths used by the product.
Can a signed-out request read or change anything valuable?
Can user A access user B’s record by changing an ID?
Can a valid member of tenant A access tenant B?
Can a client or ordinary member call an owner-only mutation?
Does access stop when a membership is suspended or removed?
The five questions a founder should be able to answer
There is an inventory of every exposed table and storage bucket.
Each important record has a plain-English user or tenant relationship.
Service-role and provider secrets exist only in trusted environments.
Privileged functions are known, narrow and tested.
Cross-user, cross-tenant and lower-role attempts have been executed and recorded.
“A policy that looks correct is an assumption. A denied request from the wrong account is evidence.”
Common questions
What founders ask next
Does Supabase enable RLS automatically?
The platform encourages RLS and applies defaults in some creation paths, but the owner remains responsible for every exposed table, policy, grant, function and storage rule. Always verify the deployed database directly.
Can I rely on auth.uid() in every policy?
It is a useful identity primitive, but the policy still needs the correct ownership or tenant relationship. You must also decide how null, inactive and privileged contexts should behave.
Is a Supabase security review a penetration test?
No. Reviewing RLS, grants, functions, storage and credential boundaries is an important production-readiness activity, but a formal penetration test is a separate specialist engagement.
Apply this to your product
Get an independent ship, remediate or rebuild verdict.
The ShipReady Audit reviews the critical journey, permissions, payments, deployment, mobile behavior and operating controls in one fixed 24–48 hour scope.
Request the $750 audit