Lovable RLS & Auth: Getting Access Control Right
Row-Level Security is the single most important security control in a Supabase-backed Lovable app, and also the most commonly misconfigured one. This guide covers the engineering facts: how RLS evaluation works, the four-policy CRUD template every Lovable app needs, how to fix the 'Infinite recursion detected in policy' error, and the precise boundary between the anon key and the service_role key. No theory — only what you need to configure it correctly.
By Founder Name · Last verified: 2026-06-23
How does Supabase RLS actually work?
Supabase Row-Level Security runs inside PostgreSQL, evaluated per row before any data leaves the database. When a client queries using the anon key, Supabase attaches the user's JWT to the request context. The RLS policy evaluates auth.uid() — the user ID from that JWT — against each row. If the expression returns false, that row is invisible to the query as if it does not exist.
This means RLS is not optional extra hardening — it is the only thing standing between one user's data and another's when your app uses the client-side Supabase SDK. The anon key, by design, respects RLS. The service_role key, by design, bypasses it. Which key your client uses determines whether your RLS policies run at all.
What does a complete four-policy RLS template look like?
The simplest and safest pattern for most Lovable apps is a standard set of four policies per user-data table: SELECT limited to the owner's rows, INSERT limited to the owner's user_id, UPDATE limited to the owner's rows, and no DELETE policy unless the feature explicitly requires it. This pattern is correct for the vast majority of Lovable use cases where each user owns their own data.
- Enable RLS: ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
- SELECT policy: CREATE POLICY 'users_select_own' ON your_table FOR SELECT USING (user_id = auth.uid());
- INSERT policy: CREATE POLICY 'users_insert_own' ON your_table FOR INSERT WITH CHECK (user_id = auth.uid());
- UPDATE policy: CREATE POLICY 'users_update_own' ON your_table FOR UPDATE USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
- Test each policy: in the Supabase SQL editor run SET LOCAL role = authenticated; SET LOCAL request.jwt.claims = '{"sub": "<user-uuid>"}'; then run your query.
| Operation | Policy name | SQL pattern |
|---|---|---|
| SELECT | users_select_own | FOR SELECT USING (user_id = auth.uid()) |
| INSERT | users_insert_own | FOR INSERT WITH CHECK (user_id = auth.uid()) |
| UPDATE | users_update_own | FOR UPDATE USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()) |
| DELETE | users_delete_own (add only if needed) | FOR DELETE USING (user_id = auth.uid()) |
What causes 'Infinite recursion detected in policy'?
This error appears when an RLS policy queries the same table it is protecting. A common example is a policy on a profiles table that runs SELECT from profiles to determine whether the current user is an admin. Supabase must evaluate the policy before allowing the query, but evaluating the policy requires running a query against the same table, which triggers the same policy again — an infinite loop. The table locks until the policy is corrected.
- Identify the recursive policy: SELECT * FROM pg_policies WHERE tablename = 'your_table' AND qual LIKE '%your_table%';
- Rewrite the policy to use JWT claims instead of a subquery. Store the user's role in the JWT at sign-in rather than looking it up during query evaluation.
- If admin status must be looked up at query time, use a separate roles table with its own non-recursive SELECT policy, and reference that in your main table's policy with SECURITY DEFINER to avoid the recursion.
- After rewriting, verify the fix: run a simple SELECT in the SQL editor under the affected role to confirm the recursion error is gone.
- Test with a non-admin user and an admin user separately to ensure both roles behave correctly.
What is the difference between the anon key and the service_role key?
The anon key is the public client key: it is safe to include in browser-side JavaScript, it respects all RLS policies, and it is what the Supabase client constructor should use in client-side code. The service_role key bypasses all RLS — it can read, write, and delete any row in any table without restriction. It is intended for trusted server-side operations only: edge functions, server actions, background jobs. It must never appear in client-side code.
Independent researchers have documented that Lovable-generated apps can ship the Supabase service_role key — which bypasses Row-Level Security — into the client-side JavaScript bundle; one April 2026 audit of 50 apps found this in 34% of them (source: Tomer Goldstein, DEV.to, April 2026). If a user opens your app's network traffic or the bundled JS, they can extract that key and query your entire database with no access controls. Search your repo for 'service_role' and 'SUPABASE_SERVICE_ROLE_KEY' to confirm it does not appear in any client-side file.
When should I bring in an expert to review my RLS setup?
RLS mistakes are silent: they do not cause build errors, do not appear in the Lovable preview, and do not surface until a user (or an attacker) queries data they should not be able to see. If your app handles payment data, health information, messages between users, or any personally identifiable data, a professional RLS audit before launch is the only reliable way to confirm the policy set is correct and complete.
An audit checks more than whether RLS is enabled — it tests whether the policies are logically correct, whether there are any tables that were accidentally left unprotected, whether your edge functions use the correct key for each operation, and whether your auth guards on the front-end routes match the database-level restrictions. Book a call and we can scope the review for your app's specific data model.
Frequently asked questions
Why can Lovable users see each other's data?
What's the difference between the anon key and the service_role key?
How do I know if my Lovable app is secure?
Can auth.uid() be spoofed by a client?
What is the 'infinite recursion' error in Supabase RLS?
Do I need a DELETE policy on every table?
What happens if I enable RLS but don't add any policies?
Can RLS protect against a compromised service_role key?
How do I test that my RLS policies are correct?
Talk to a senior engineer — not a salesperson.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.