Supabase row level security mistakes AI-built apps leave open, and how to check yours
The most serious gap we look for in a Supabase app is a table that anyone can read or change through the API. It happens when row level security (RLS) is off on a table in an exposed schema, or when a policy is switched on but lets everyone through. Supabase is plain about it: a table in an exposed schema without RLS is readable and writable by any role with a grant on it (Supabase, Row Level Security). Supabase's Security Advisor flags most of these problems, and a simple test with two accounts confirms the rest.
The key in your browser is not the problem
People often panic when they see a Supabase key in their page source. The publishable key, which replaces the older anon key, is designed to be public; Supabase lists web pages and source code among the places it is safe (Supabase, API keys). RLS decides what that key can reach. The secret key, which replaces service_role, is different: it bypasses RLS, so it belongs only in server code. Lovable's docs make the same point: your backend address and publishable key are visible in the browser, and your policies must protect the data (Lovable, Control who can access your app's data).
Seven mistakes to look for
- RLS is off on a public table. Supabase's advisor reports this as an error named rls_disabled_in_public. Supabase also warns that existing projects may grant the anon and authenticated roles every privilege by default, and that adding policies does not remove those grants.
- A policy exists but RLS is not enabled. The policy looks reassuring in the dashboard, but nothing enforces it. The advisor calls this policy_exists_rls_disabled.
- A policy lets everyone through. An always-true rule such as USING (true) is flagged as permissive_rls_policy. A quieter version is a policy that only checks that someone is signed in. Lovable notes this gives every signed-in user the same access to every row; to separate users' data, the policy must compare the requester with something in the row, such as its owner.
- Inserts and updates are not checked. USING decides which existing rows a user can see or change; WITH CHECK decides whether a new or changed row is allowed. If an insert policy does not compare the row's owner with the signed-in user, someone can write rows in another user's name. Supabase's pattern is (select auth.uid()) = user_id, and it notes that auth.uid() is null for a visitor who is not signed in.
- A policy trusts user metadata. Supabase says raw_user_meta_data can be updated by the signed-in user and is not a good place for authorisation data. Roles such as admin belong in raw_app_meta_data, which users cannot change. The advisor flags rls_references_user_metadata.
- A view or function skips RLS. Views bypass RLS by default because they usually run as the postgres user; on Postgres 15 and later, create them with security_invoker = true. Security definer functions run with their creator's rights, and Supabase says never to create one in an exposed schema.
- The secret key is in browser code. Lovable warns that "that key skips every policy you have." If it has leaked, rotate it after fixing the cause; Supabase notes an attacker can still use a leaked key from other tools.
How to check yours
- Supabase Security Advisor. Open Advisors, then Security, in the Supabase dashboard, or run supabase db advisors from the CLI (Supabase, Database advisors). Supabase notes some findings may be intentional, so check each against what the table is for.
- On Lovable. Review policies under More, Database, Manage, Policies. The view is read-only, and Lovable says it is not a full report. A Quick scan runs automatically when you publish and flags tables without per-record access control and rules that let everyone through; a Deep scan has to be started yourself (Lovable, Security overview).
- On Bolt. Choose Publish, then Run security audit on a paid plan, or open the Security tab of your database settings on any plan (Bolt help, Check your project's security). Bolt says restoring an earlier version also removes the security changes it made.
Scanners help, but both Lovable and Bolt stop short of promising complete security. Our own test, which takes a few minutes: create two test accounts. As user A, add a record. As user B, try to open, edit and delete it, including by changing an ID in the address bar. Then sign out and repeat in a private window. Anything that works and should not is a policy to fix.
Fixing it without breaking the app
Our advice: change one table at a time, starting with tables holding personal or payment data, and run the two-account test after each change. Turning RLS on with no policies makes a table return nothing through the API, so expect parts of the app to go empty until the right policies exist. That is safer than open data, but plan for it. Our note on stabilising an AI-built app covers making changes in small, reversible steps.
Want it checked for you?
The AI-Built App Audit ($299 USD) is a report-only review of sign-in, data access, exposed keys and payments, ranked by risk. App Rescue and Move to Cloudflare fixes security in its Rescue in place step for a fixed $999 USD. App Triage ($99 USD) is the smallest first step. Related notes: Lovable login not working after you publish and a Bolt app that breaks when published.
Sources
- Row Level Security, Supabase docs
- Understanding API keys, Supabase docs
- Database advisors, Supabase docs
- Control who can access your app's data, Lovable docs
- Security overview, Lovable docs
- Check your project's security, Bolt help centre